【RTOS学习】任务创建 | 任务启动 | 任务切换 | 任务暂停和恢复 | 任务阻塞和唤醒 | 临界资源保护

news2024/11/15 19:33:48

🐱作者:一只大喵咪1201
🐱专栏:《RTOS学习》
🔥格言:你只管努力,剩下的交给时间!

图

目录

  • 🌏任务创建
    • 🧭TCB和栈
    • 🧭伪造现场
    • 🧭链表操作
  • 🌏任务启动
    • 🧭创建空闲任务和定时器任务
    • 🧭启动调度器
  • 🌏任务切换
  • 🌏任务的暂停和恢复
    • 🧭暂停
    • 🧭恢复
  • 🌏任务的阻塞和唤醒
    • 🧭阻塞
    • 🧭唤醒
  • 🌏临界资源保护
    • 🧭关中断
    • 🧭关闭调度器
  • 🌏总结

前面认识了FreeRTOS中的链表和堆的管理后,接下来再看看FreeRTOS和任务相关的内容。

🌏任务创建

我们知道,在FreeRTOS中可以存在很多任务,每一个任务都有很多的属性,这些属性构成一个TCB结构体,专门用来描述一个任务。

tu
如上图所示是TCB结构体的定义,它包含的主要成员变量有:

  • volatile StackType_t * pxTopOfStack:当前任务栈的栈顶位置,该位置指向栈中最后入栈的元素。
  • ListItem_t xStateListItem:这是一个用来管理任务状态的链表项。
  • ListItem_t xEventListItem:这是一个用来管理任务事件的链表项。
  • UBaseType_t uxPriority:用来表示当前任务的优先级。
  • StackType_t * pxStack:当前任务栈的最低位置,表示栈的最大容量。
  • char pcTaskName[ configMAX_TASK_NAME_LEN ]:用来存放任务名称。

图
如上图所示,在创建任务之前,都会创建一个TaskHandle_t类型的任务句柄,该句柄本质上就是TCB*,是TCB结构体指针的类型重命名。

在调用xTaskCreate创建任务的本质就是在该函数中填充这个TCB结构体,在创建任务时,主要做这几件事:

  • xTaskCreate创建动态任务:
    • 分配TCB结构体
    • 分配栈
    • 伪造现场:构造栈的内容
    • 把TCB放入就绪链表
  • xTaskCreateStatic创建静态任务:
    • 伪造现场:构造栈的内容
    • 把TCB放入就绪链表

动态创建和静态创建相比,静态创建少了为任务分配TCB结构体和栈这两步,因为静态创建时,TCB结构体不在堆上,栈也是由用户指定的。

🧭TCB和栈

动态创建:

图
如上图代码所示,适用xTaskCreate动态创建任务时,在函数内部会调用pvPortMalloc函数在堆区上申请存放TCB结构体变量和任务所需要栈的空间。

而且还让TCB结构体中的pxStack成员指向栈空间的最低地址(这块内存的起始地址,也是栈的最大存储位置)。

静态创建:

tu

如上图代码所示,适用xTaskCreateStatic静态创建任务时,并没有从堆区上申请TCB和栈所用的空间,而是直接将调用函数时传入的TCB结构体变量pxTaskBuffer和栈puxStackBuffer赋值给pxNewTCB

🧭伪造现场

静态创建和动态创建都有伪造现场,而且在这一点上的做法是一致的:

tu
如上图代码所示,在完成TCB变量和栈的创建以后,在xTaskCreate任务创建函数中调用prvInitialiseNewTask来初始化TCB。

tu
如上图所示prvInitialiseNewTask函数,在该函数中进行TCB的初始化,主要操作有三步:

  1. 计算栈顶pxTopOfStack

由于栈是一个连续的数组,所以pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ]得到的就是栈的最大地址处,也就是栈顶。除此之外,还要对栈顶进行对齐处理。

  1. 处理优先级

如果传入的优先级大于可以设置的最大优先级configMAX_PRIORITIES,就将优先级设置为configMAX_PRIORITIES - 1。还有一些任务名字的处理等内容比较简单,本喵就不讲解了。

  1. 初始化栈

会调用pxPortInitialiseStack函数来初始化栈,也就是进行现场伪造:

tu
如上图代码所示,在函数中进行现场伪造,portINITIAL_XPSR就是寄存器xPSR的值,( StackType_t ) pxCode + portINSTRUCTION_SIZE就是任务函数的地址,给LR赋值prvTaskExitError,表示错误返回执行的函数地址,一般不会执行该函数。pvParameters是创建任务时传给任务函数的那个参数,放在R0中。

  • 无所谓什么值的寄存器,在伪造现场时压根没有处理它,只是在移动栈顶。
  • 最后返回的栈顶位置存放的是伪造的R4寄存器值。

TU
如上图所示便是伪造完现场后的结果。


🧭链表操作

图

如上图所示,在task.c中有一个static List_t pxReadyTasksLists[ configMAX_PRIORITIES ]类型数组,该数组有configMAX_PRIORITIES 个元素,也就是最大优先级是多少,就有多少个元素。

  • 每个元素的类型都是一个List_t链表头。
  • 每一个优先级对应一个就绪队列。

TU
如上图所示,前面的五个链表头List_t就是就绪队列数组中的五个元素,每个链表头都代表这一个队列,而链表头所在位置的下标就是该队列中所有任务的优先级。

以优先级为1为例,可以看到,三个TCB结构体通过它们的成员变量xStateListItem链表项,将自己链接到了该队列中。通过链表项中的pOwner可以找到链表项所属的TCB变量。

  • 链表项xStateListItem就像是一个铭牌,每个TCB都有一个铭牌,该铭牌位于就绪链表中。

tu
如上图所示,在xTaskCreate中创建任务时,还要进行链表操作,调用prvAddNewTaskToReadyList将创建的新TCB链入到对应的就绪链表中。

图
如上图代码所示,在插入链表之前,要设置一些链表项中的值:

  1. 初始化当前TCB中的链表项
  2. 设置链表项中的pOwner,使得通过链表项可以找到当前TCB。
  3. 设置链表项中的Value,当进行排序时会用到该值。

tu
如上图代码所示,设置好链表项中的值以后,调用prvAddTaskToReadyList将包含链表项的TCB插入到就绪队链表中,在该函数中再调用listINSERT_END将该TCB插入到对应数组下标链表头所维护的链表中。

图
如上图所示listINSERT_END宏函数,在该函数中进行尾插,并且让链表项中的pxContainer指向链表头,将链表头中链表项个数加一。

  • 尾插的过程中要保证公平性

此时一个任务就创建好了。

🌏任务启动

任务创建好以后,需要调用vTaskStartScheduler函数来启动任务,也就是开启调度器。

🧭创建空闲任务和定时器任务

  1. 创建空闲任务

在FreeRTOS中,除了我们创建的任务外,还有一个空闲任务,该任务的优先级是最低的,当所有用户任务都不在运行时,就会运行空闲任务,该任务进行回收用户任务资源等操作。

tu
如上图所示vTaskStartScheduler,在该函数中,如果支持静态创建就创建一个静态的空闲任务,如果不支持就动态创建一个空闲任务。

  1. 创建定时器任务

tu
如上图所示,如果配置了使用定时器的话,在启动调度器的时候还会创建一个定时器任务。

图

如上图所示,此时优先级为0的就绪链表中就会有一个空闲任务Idle_Task

🧭启动调度器

  1. 设置PendSVSysTick中断为最低优先级

tu
如上图,在创建好空闲任务和定时器任务后,会调用xPortStartScheduler函数来启动调度器。

tu

如上图所示,在xPortStartScheduler函数中,会设置NVIC中优先级寄存器,让PendSVSysTick中断为最低优先级。

图
如上图所示,此时中断向量表中的PendSVSysTick中断就被设置成了最低优先级,优先级相同,无法抢占

  1. 使能SysTick中断

在设置完优先级以后会调用vPortSetupTimerInterrupt函数来使能SysTick中断:

tu
如上图代码所示,在vPortSetupTimerInterrupt函数中配置SysTick时钟频率,并且使能SysTick定时器。

portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL,这里设置重装载值时,为什么会减1呢?

图
如上图所示,假设要3个CLK产生一次中断,由于SysyTick是向下计数的,所以刚开始启动时,从2减到0需要2个CLK,接下来需要重新装载计数值。

装置计数值会耗费一个CLK的时间,所以当计数值重新变成2时已经消耗1个CLK,然后再计数两个CLK,一共就3个CLK了,如此往复。

对于72MHZ的CLK来说,如果想让1ms产生一次中断,装置值就需要设置成72000000/1000 - 1

  1. 触发SVC异常,启动第一个任务。

在调用完vPortSetupTimerInterrupt函数启动了SysTick定时器以后,会再调用prvStartFirstTask来启动第一个任务:

tu
如上图代码所示,vPortSetupTimerInterrupt是一个汇编函数,在里面进行了四步操作:

  1. 获取默认的栈顶

图
如上图所示,从向量表的第一项中获得默认栈的栈顶__initial_sp

  1. 将获取到的栈赋值给MSP
  2. 打开CPU中断,运行处理所有中断。
  3. 触发SVC异常。

使用汇编指令SVC 0来触发SVC异常,第一次任务运行前的的现场恢复是在SVC异常中进行的:

tu
如上图vPortSVCHandlerSVC异常处理函数,这也是一个汇编函数,在该函数中恢复创建任务时伪造的现场:

  1. 获取当前TCB中的栈顶

在FreeRTOS中存在一个pxCurrentTCB全局变量,该变量指向的是正在执行任务的TCB,在创建任务时,该变量指向最后创建的最高优先级的任务。
tu
如上图所示,在TCB结构体中,第一个成员变量就是pxTopOfStack栈顶指针,所以用汇编指令读取TCB结构体中的起始4字节得到就是该任务的栈顶。

  1. 软件恢复R4~R11
  2. 将栈顶赋值给PSP

为了任务处理更加高效,任务的栈使用PSP寄存器来维护,但是此时SP仍然使用的是MSP,因为任务还没有启动。

  1. 让CPU能处理所有中断
  2. 修改LR中的特殊值

由于这是SVC异常处理函数,所以LR寄存器中的值是一个特殊值,该值用来触发硬件恢复,这里要修改一下这个特殊值:

tu
如上图,要确保特殊值中的bit3是1,表示异常返回后是线程模式,还要确保bit2是1,表示异常返回后使用的是线程栈PSP

  1. 触发硬件恢复

此时软件恢复已经完成,使用BX R14触发硬件恢复,自此创建任务时伪造的现场全部从当前任务pxCurrentTCB的栈中恢复到CPU的寄存器里了。

而且程序跳转到了前任务pxCurrentTCB函数处开始指向。

  • 第一次启动任务是通过SVC异常启动的。

🌏任务切换

此时已经有任务在运行了,当产生SysTick中断的时候就会切换任务:

  1. SysTick中断函数

tu

如上图所示xPortSysTickHandler中断函数,在进入该函数时,禁止CPU处理优先级中断(允许使用系统调用的中断),然后软件触发PendSV中断,在PendSV中断函数中发生任务切换。

  • 由于SysTick中断和PendSV中断的优先级相同,所以在SysTick中触发PendSV中断时无法抢占执行。
  • 等允许优先级中断产生后,退出SysTick中断后接着执行PendSV中断函数。
  1. PendSV中断函数

图

如上图所示xPortPendSVHandlerPendSV中断处理函数,这是一个汇编函数,在该函数中完成现场恢复:

  1. 获取当前任务pxCurrentTCB的PSP栈顶
  2. 软件保存R4~R11寄存器中的值到当前任务pxCurrentTCB的栈中。

在保存完寄存器的值以后,将当前任务的栈顶保存到栈中存放R4的位置。

  1. pxCurrentTCB的地址和LR中的特殊值保存到MSP中。

由于这是在PendSV中断函数中,所以中断函数使用的栈必然是MSP。因为在切换恢复新任务的现场时会用到R3LR寄存器。

  1. 调用vTaskSwitchContext挑选下一个任务

tu
在该函数中再调用taskSELECT_HIGHEST_PRIORITY_TASK从所有就绪链表中挑选出优先级最高的任务。

tu
如上图所示,在taskSELECT_HIGHEST_PRIORITY_TASK函数中,遍历所有链表,从优先级最高的链表开始遍历,找到不为空的链表,然后调用listGET_OWNER_OF_NEXT_ENTRY函数获得链表中要执行的新任务。

  • 遍历链表时,并不是直接从系统允许的最高优先级开始遍历,而是从现有任务的最高优先级链表开始向低优先级链表遍历。

tu
如上图所示代码中,在一进入listGET_OWNER_OF_NEXT_ENTRY函数中,就让pxIndex迭代到下一个链表项,如果迭代后的链表项指向了链表头,那么继续迭代一次,指向第一个链表项。

最后将pxIndex指向的新TCB赋值给pxCurrentTCB

  1. 从MSP中获取pxCurrentTCBLR的值。

挑选完下一个要执行的任务后,从MSP中读取pxCurrentTCBLR,由于保存的时候,保存的是pxCurrentTCB的地址到MSP中,在挑选任务时改变的是pxCurrentTCB本身。

所以可以从MSP中读取到的pxCurrentTCB的地址找到pxCurrentTCB本身。LR这个特殊值也要从MSP中恢复出来。

  1. 软件恢复R4~R11

读取新任务pxCurrentTCB中的头4个字节,得到的是新任务的栈顶pxTopOfStack,然后从新任务的栈中进行软件恢复。

  1. 触发硬件恢复。

此时任务就完成了切换,可以看到,任务的切换并不是在SysTick中断中完成的,而是在SysTick中触发的PendSV中完成的。

而第一次启动任务时,是在vTaskStartScheduler中启动调度器时,触发SVC异常,在异常处理函数中完成第一次现场切换的。

为什么第一次启动和后面的任务切换不都在SysTick中断中完成呢?这样更方便啊,本喵之前自己模拟的任务切换就是这样完成的。

  • 发生任务切换的场景并不都是时间片轮转到了。

当任务阻塞时会主动发起任务调度把自己切换下去,如果在SysTick中断完成的话,就无法主动切换任务,而在PendSV中断中完成的话,可以直接触发PendSV中断来完成任务切换。

  • 任务切换的过程为:保护原任务的现场 -> 挑选要执行的新任务 -> 恢复新任务的现场。

🌏任务的暂停和恢复

图
如上图所示,存在一个xSuspendedTaskList链表,用来管理处于暂停任务的状态,该链表中的TCB的存放没有顺序,因为不需要排序,唤醒时是指定唤醒的。

🧭暂停

tu
如上图任务状态转换图,让一个任务变成暂停状态有三种情况:

  1. 暂停就绪链表中的任务
  2. 暂停自己
  3. 暂停阻塞链表中的任务

图
如上图所示vTaskSuspend函数,在该函数中,先调用prvGetTCBFromHandle获得要移除的任务句柄,这是一个宏。然后调用uxListRemove函数将任务的TCB从它当前的链表中移除。

tu
如上图uxListRemove函数,在该函数中,先获取要移除TCB所在的链表头pxContainer,然后从链表中移除。

如果移除的TCB是当前正在操作的链表,则让pxIndex指向被移除TCB的前一个TCB,这种情况是自己暂停自己。

之后让移除的TCB失忆,让其pxContainer = NULL,然后再将链表头中记录链表项的uxNUmberOfItems减一。

tu
如上图所示代码,继续vTaskSuspend函数讲解,如果要移除的任务处于某个事件链表中,则也要移除。将被移除的任务TCB尾插到暂停链表xSuspendedTaskList中。

图
如上图所示,如果被暂停的是正在执行的任务,也就是自己暂停自己,那么就主动发起一次调度,去执行就绪链表中的下一个任务。

🧭恢复

图

如上图vTaskResume函数所示,在函数内部,如果要唤醒的任务是处于暂停状态,那么就调用uxListRemove函数从暂停链表中移除,然后再将该任务插入到就绪链表中。

  • 插入就绪链表中的操作,和创建任务时插入的操作一样。

还要判断一下,唤醒任务的优先级如果大于等于当前正在执行的任务,那么就立刻发起一次调度,去执行这个被唤醒的任务。

🌏任务的阻塞和唤醒

tu
如上图所示,存在两个延时链表pxDelayedTaskListpxOverflowDelayedTaskList,这两个链表都是用来管理因调用vTaskDelay函数而处于阻塞状态的任务的。

🧭阻塞

ti
如上图所示,只有正在处于运行状态的任务才能进入阻塞状态,最常见的阻塞就是调用vTaskDelay延时函数:

图
如上图所示vTaskDelay函数,在该函数中,先调用prvAddCurrentTaskToDelayedList函数将要阻塞的任务插入到pxDelayedTaskList链表中,然后再调用portYIELD_WITHIN_API主动发起一次调度。

tu
如上图所示prvAddCurrentTaskToDelayedList部分函数,主要进行了三步操作:

  1. 将调用vTaskDelay的当前任务从就绪链表中移除。
  2. 如果要阻塞的任务是无限阻塞,则放入暂停链表。
  3. 如果阻塞一段时间,则计算唤醒时间xTimeToWake,并设置到链表项中。

在延时链表中,所有任务的TCB按照链表项中的唤醒时间升序排列,越接近链表头的TCB,唤醒时间就越靠前。所以只需要判断链表中第一个TCB的超时时间是否达到就可以了。

图
如上图所示后续代码,要判断一下超时时间是否溢出了,如果溢出则放入pxOverflowDelayedTaskList链表,没有超时则放入pxDelayedTaskList链表。

每产生一次SysTick中断,全局变量xTickCount都会加一,该变量代表着系统时间,但是这是一个unsigned int类型的变量。

在计算唤醒时间xTimeToWake = xConstTickCount + xTicksToWait后,如果溢出了,则得到的时间会小于当前时间,此时就不能再插入到pxDelayedTaskList链表中,而是要插入专门管理溢出的延时链表pxOverflowDelayedTaskList中。

tu
如上图,再更新一下唤醒时间xNextTaskUnblockTime,要保证该变量是最小值

🧭唤醒

唤醒时间到了又是如何做的呢?谁去判断,谁去唤醒呢?

tu
如上图xPortSysTickHandler中断函数所示,在每产生一次SysTick中断以后,就会调用一次xTaskIncrementTick函数,在这个函数中进行判断是否唤醒阻塞的任务,以及进行唤醒操作。

图
如上图xTaskIncrementTick部分代码所示,每产生一次SysTick中断就会给系统时间加一,当计数值发生溢出时,调用taskSWITCH_DELAYED_LISTS交换两个延时链表的链表头,使用原本的溢出延时链表。

tu
如上图代码所示,当系统时间xConstTickCount大于下一个任务的唤醒时间xNextTaskUnblockTime时,在一个死循环for中进行操作。

在循环中,调用listGET_OWNER_OF_HEAD_ENTRY函数获得延时链表中要被唤醒的任务:

#define listGET_OWNER_OF_HEAD_ENTRY( pxList )   ( ( &( ( pxList )->xListEnd ) )->pxNext->pvOwner )

这是一个宏函数,直接获取延时链表中第一个任务TCB即可,因为是按照唤醒时间排序的。

之所以在循环中进行,是为了将延时链表中延时相同时间的任务一起唤醒,当xConstTickCount < xItemValue时,说明此时所有要唤醒的任务都唤醒了,跳出循环。

图
如上图所示,在循环中每挑出一个要唤醒的任务时,都要将其从阻塞链表中移除,并且将其插入到就绪链表中。

放入到就绪链表中后,要判断一下唤醒的任务优先级是否大于等于正在运行的任务优先级,如果大于等于则将xSwitchRequired设置为真,当退出SysTick中断后就会发起调度,让唤醒的这个高优先级任务去运行。

  • 阻塞任务的唤醒和判断是由SysTick中断完成的。

🌏临界资源保护

保护临界资源的方法原则:谁可能跟我竞争,就防患于未然禁止谁。

  • 我是任务A,任务B可能跟我竞争,那就"先关闭调度器,再访问临界资源,最后开启调度器"
  • 我是任务A,中断函数可能跟我竞争,那就"先关闭中断,再访问临界资源,最后开中断"
  • 我是任务A,任务B或中断都可能跟我竞争,那就"先关闭中断,再访问临界资源,最后开中断"
  • 我是中断A,中断B可能跟我竞争,那就"先关闭中断,再访问临界资源,最后开中断"

所以说保护临界有关调度器关中断两种方式。

🧭关中断

tu
如上图所示,在进入临界区时,调用taskENTER_CRITICAL来关闭中断。

TU
如上图所示,关闭中断时最终会调用portDISABLE_INTERRUPTS函数来关闭中断,该函数是一个汇编函数,会禁止优先级小于ulNewBASEPRI的所有中断。

在关闭中断以后,会让uxCriticalNesting变量加加,这是一个全局变量,只要该值不为0,说说明中断处于关闭状态。

tu
关闭中断时不能关闭所有中断,只关闭优先级低于ulNewBASEPRI的中断,这些中断也被称为允许使用系统调用FromISR的中断,优先级比这些高的中断是更加紧急的中断,是不能禁止的。

  • 在关闭中断期间,SysTick中断和PendSV中断无法产生,就不会发生任务切换,就没有人来竞争。

中断关闭以后,就可以正式访问临界区了,在访问完临界区以后,要调用prvResetNextTaskUnblockTime函数来更新延时链表中的唤醒时间。

最后再调用taskEXIT_CRITICAL开启中断:

tu

如上图代码所示,在开启中断时,会调用vPortExitCritical函数,在该函数中会先对uxCriticalNesting减一,直到为0后才调用portENABLE_INTERRUPTS函数恢复中断。

  • uxCriticalNesting为0时才能恢复中断,因为禁止中断的地方可能不止一处。

🧭关闭调度器

关闭调度器使用的是vTaskSuspendAll函数:

tu
如上图,在vTaskSuspendAll函数中,仅仅是将全局变量uxSchedulerSuspended加一。秘密就在xTaskIncrementTick函数中。

tu
如上图所示,在SysTick中断函数中会调用xTaskIncrementTick函数,在该函数中,会判断uxSchedulerSuspended的值,如果该值为0,说明没有关闭调度器,就会进行后续的操作,增加系统时间,唤醒阻塞任务等等。

但是调用vTaskSuspendAll关闭了调度器以后uxSchedulerSuspended值就不是0了,就不会执行原本的操作了,而且此时的返回值xSwitchRequired = pdFALSE

当该函数返回以后,SysTick中断函数中也不会触发PendSV中断,也不会发生任务的切换。

  • uxSchedulerSuspended不为0,调度器就处于关闭状态,就不会发生任务切换。

打开调度器使用的是xTaskResumeAll函数:

tu
如上图代码所示,唤醒任务时,先对uxSchedulerSuspended全局变量进行减1,只有当这个变量为0时,才会重新打开调度器。

恢复xPendingReadyList链表中的任务到就绪链表中:
tu
如上图代码所示,在打开调度器时,先会遍历一个xPendingReadyList链表,将该链表中的所有任务都放入到就绪链表中,并且根据优先级决定是否立刻发起调度。


xPendingReadyList链表又是干什么的呢?在关闭调度器的期间,只是不能发生任务切换,但是中断还是可以产生的。

假设现在有一个空队列,原本队列的xEventList链表中有很多任务在等待,在调度器关闭期间,使用FromISR后缀的系统调用向队列中写入了数据,此时本应该会唤醒该队列中等待的任务去读取数据的。

但是此时由于调度器是关闭的,将等待数据的任务放入到就绪链表中也没有意义,因为无法立刻发起调度,所有就先将这些原本等待的任务放入到了xPendingReadyList链表中。


处理阻塞链表中的任务:

tu
如上图所示,首先更新阻塞链表中的唤醒时间,由于在关闭调度器期间,SysTick中断仍然可以发生,系统时间仍然在流逝,当阻塞链表中的最小唤醒时间和系统时间相等时也无法唤醒阻塞链表中的任务。

所以在关闭调度器期间,真正的系统时间不增加,而是增加一个假的时间xPendedCounts

tu
如上图所示,在调度器关闭期间,SysTick中断仍然会产生,xTaskIncrementTick函数也仍然会调用,但是此时在该函数中并不进行阻塞任务的唤醒操作,也不增加系统时间xTickCount,而是仅增加一个假的系统时间xPendedCounts

所以在打开调度器的时候,变量xPendedCounts表示调度器的关闭时长,所以在do循环中不断调用xTaskIncrementTick函数来模拟系统时间的增加。每循环一次xPendedCounts减一。

在模拟过程中会很快的将xPendedCounts假系统时间消耗完毕,并且会进行阻塞任务的唤醒操作。

  • 使用关闭中断的方式来保护临界资源的代价有点大,所以尽量使用闭关调度器的方式来保护临界资源。

🌏总结

分析了FreeRTOS源码中的任务创建,任务启动,任务切换,任务暂停和恢复,任务阻塞和唤醒以及临界资源的保护。要深刻体会到不同类型链表的作用,认识到不同任务的本质就是处于不同类型的链表中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1310133.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

solidity 特性导致的漏洞

目录 1、默认可见性 2、浮点数精度缺失 3、错误的构造函数 4、自毁函数 5、未初始化指针-状态变量覆盖 1、默认可见性 Solidity 的函数和状态变量有四种可见性&#xff1a;external、public、internal、private。函数可见性默认为 public&#xff0c;状态变量可见性默认为…

51单片机控制1602LCD显示屏输出自定义字符二

51单片机控制1602LCD显示屏输出自定义字符二 1.概述 1602LCD除了内置的字符外还提供自定义字符功能&#xff0c;当内置的字符中没有我们想要输出的字符时&#xff0c;我们就可以自己创造字符让他显示&#xff0c;下面介绍1602如何创建自定义字符。 2.1602LCD创建字符原理 自…

2023 re:Invent使用 PartyRock 和 Amazon Bedrock 安全高效构建 AI 应用程序

前言 本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 “Your Data, Your AI, Your Future.&#xff08;你的数据&#xff0c;你的AI&…

利用Microsoft Visual Studio Installer Projects打包安装包

利用Microsoft Visual Studio Installer Projects打包安装包 具体步骤步骤1&#xff1a;安装扩展步骤2&#xff1a;创建 Setup 项目步骤3&#xff1a;设置属性步骤4&#xff1a;添加输出步骤5&#xff1a;添加文件步骤6&#xff1a;添加桌面快捷方式步骤7&#xff1a;添加菜单快…

使用Pytorch从零开始构建StyleGAN

本文介绍的是当今最好的 GAN 之一&#xff0c;来自论文《A Style-Based Generator Architecture for Generative Adversarial Networks》的 StyleGAN &#xff0c;我们将使用 PyTorch 对其进行干净、简单且可读的实现&#xff0c;并尝试尽可能接近原始论文。 如果您没有阅读过…

如何实现电脑文件夹自动备份?以下是图解教程

在当今迅猛发展的科技时代&#xff0c;电脑已经成为不可或缺的办公工具。随着使用时间的增加&#xff0c;存储在电脑中的文件数量也逐渐增多。然而&#xff0c;由于设备故障、手动误删等原因&#xff0c;文件的丢失问题成为一个不可忽视的风险。如果丢失的文件具有重要性&#…

文字转语音自动合成系统源码:让你的语音自动转成文字 附带完整的搭建教程

人工智能技术的不断发展&#xff0c;语音识别和自然语言处理技术已经逐渐成熟。文字转语音自动合成系统就是结合了这两项技术&#xff0c;将文字信息转化为语音输出&#xff0c;为用户提供更加便捷、高效的信息获取方式。这种系统在语音助手、智能客服、教育学习等领域有着广泛…

54 代码审计-TP5框架审计写法分析及代码追踪

目录 知识点1知识点2演示案例:demo代码段自写和规则写分析hsycms-TP框架-不安全写法-未过滤weipan21-TP框架-规则写法-内置过滤 知识点1 调试&#xff0c;访问&#xff0c;路由&#xff0c;配置&#xff0c;版本等 知识点2 自写写法&#xff1a;自己写代码&#xff0c;一步步…

PyQt6 简单介绍与安装

前文&#xff0c;参考文章&#xff1a; 参考文章一 参考文章二 PyQt6 简单介绍与安装 1、简单介绍2、PyQt6安装3、PyQt6版本查看4、PyQt6模块4.1 界面承载部分( 控件 )4.2 界面框架部分&#xff08;布局&#xff09;4.3 界面组件部分&#xff08;其实也是Widget类&#xff0…

Json数据报文解析-Gson库-JsonObject类-JsonParse类-JsonArray类

一、前言 本文我们将介绍如何解析Json数据&#xff0c;主要通过Gson库中的相关类来实现。 二、详细步骤 首先&#xff0c;我们要拿到一个基础的Json数据&#xff0c;这里将以下面的Json数据作为示例&#xff1a; {"code":"1","msg":"ok&q…

电脑监控软件丨老板的“管理神器”?员工的“噩梦伊始”?

不得不承认&#xff0c;老板们都很喜欢用电脑监控软件来管控员工的工作情况。但是面对这个话题&#xff0c;他们却又有不一样的感受。 老板的“管理神器”&#xff1f;——首先来说老板 不得不说&#xff0c;老板确实很喜欢用域之盾软件--电脑管控功能https://www.yuzhidun.cn…

mfc140u.dll丢失的解决方法的详细介绍,六种解决mfc140u.dll丢失的方法

今天的这篇文章将向各位分享一个有关电脑出现关于丢失mfc140u.dll错误的弹窗问题&#xff0c;这是一个很常见的问题。无论你是一名大学生还是其他身份&#xff0c;都可能会遇到这个问题。下面我会对mfc140u.dll丢失的解决方法进行详细的介绍。 一.六种解决mfc140u.dll丢失的方法…

浪潮信息大突破:全面开源1026亿参数模型源2.0

近日&#xff0c;浪潮信息发布了一项重大成就&#xff0c;宣布全面开源其1026亿参数的基础大模型——源2.0。该举措在AI产业界引起了广泛关注&#xff0c;被视为推动生成式人工智能产业快速发展的关键一步。 源2.0模型概览 源2.0是一个多参数级别的大模型&#xff0c;提供了1…

‘BLEUUID‘ does not name a type错误怎么解决?

摘要&#xff1a;arduino环境下对esp32蓝牙编程时会遇到BLEUUID does not name a type错误&#xff0c;本文介绍解决方法。 硬件设备是安信可ESP32-S模组。 错误发生在代码最开始的地方&#xff0c;include了一个蓝牙设备头文件&#xff0c;然后定义了UUID&#xff0c;注意看&a…

Conda 使用教程大全来啦

什么是 Conda&#xff1f; Conda 是一款功能强大的软件包管理器和环境管理器&#xff0c;您可以在 Windows 的 Anaconda 提示符或 macOS 或 Linux 的终端窗口中使用命令行命令 Conda 可以快速安装、运行和更新软件包及相关依赖项。Conda 可以在本地计算机上创建、保存、加载和…

人工智能改变医疗保健:人工智能如何革命医学

人工智能&#xff08;Artificial Intelligence, 简称AI&#xff09;的快速发展正逐渐改变着我们的生活方式和社会结构。在医疗保健领域&#xff0c;AI的应用不仅提供了更准确、高效的诊断和治疗手段&#xff0c;还为医生和患者之间的交流提供了新的途径。本文将探讨人工智能如何…

计算机组成原理-ATT格式vsIntel格式

文章目录 AT&T格式 vs lntel格式 x86汇编语言是lntel格式&#xff0c;还有一种汇编语言格式是AT&T AT&T格式 vs lntel格式 lntel格式中取主存地址内容未指明长度默认为32位&#xff0c;对应下图中第四行右边的指令 百分号 美元符号 小括号 可用于计算机结构体数组…

有意思!40小时工作制来了,996再见

​在中国&#xff0c;加班文化已经深入人心。工资越高加班越多&#xff0c;“996”已成为一些行业标签&#xff0c;月薪30k以上的职场人中超过86&#xff05;经常加班。所以今天我就来说一下这40小时工作制到底是从何而来&#xff0c;感兴趣的往下看看吧&#xff01; 40小时工…

2023自动化测试框架的设计原则你都知道吗?快来看!

1.代码规范 测试框架随着业务推进&#xff0c;必然会涉及代码的二次开发&#xff0c;所以代码编写应符合通用规范&#xff0c;代码命名符合业界标准&#xff0c;并且代码层次清晰。特别在大型项目、多人协作型项目中&#xff0c;如果代码没有良好的规范&#xff0c;那么整个框架…

安装LLaMA-Factory微调chatglm3,修改自我认知

安装git clone https://github.com/hiyouga/LLaMA-Factory.git conda create -n llama_factory python3.10 conda activate llama_factory cd LLaMA-Factory pip install -r requirements.txt 之后运行 单卡训练&#xff0c; CUDA_VISIBLE_DEVICES0 python src/train_web.py…