FreeRTOS内部机制重点,看这一篇就够了。

目录

一、FreeRTOS结构

1.1.移植

1.2.核心文件

1.3.相关头文件

1.4.内存管理

1.4.1.Heap_1

1.4.2.Heap_2(最佳匹配算法)

1.4.3.Heap_3

1.4.4.Heap_4(首次适应算法)

1.4.5.Heap_5

二、任务管理

2.1.什么是任务?任务的本质

2.2.ARM架构

2.3.汇编指令

2.4.怎么保存函数的现场

2.5.要保存什么 保存现场的几种场景 任务的重点

2.6.创建任务

2.7.任务优先级和Tick

2.8.任务状态

三、两个Delay函数

四、空闲任务和钩子函数

五、分析任务中途放弃运行情况

六、同步通信与互斥_重点

6.1.各类方法的对比

七、队列的核心

八、信号量的核心

九、互斥量的核心


一、FreeRTOS结构

1.1.移植

重点: 1.Demo:CORTEX_STM32F103_Keil 芯片与编译器平台 2.Source:核心在同步通信与互斥的 .c 文件,队列 信号量 互斥量 事件组 任务通知等等;移植实现文件portable RVDS/ARM_CM3 工具链/平台处理器内核

移植的重点在于port.c(上下文切换 调度 定时器 )portmacro.h(中断管理 堆栈管理 特定编译器和架构) 其次就是 工具链/平台处理器内核 RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。

1.2.核心文件

最核心所在是tasks.c以及list.c,前者是任务调度管理,因为时间片轮转,任务调度管理都涉及,这是任务的本质,后者是链表管理,从内部机制出发,刨析队列,信号量互斥量等等,本质都是基于链表来操作的,任务状态切换也需要链表。

其他核心的文件如下

FreeRTOS/Source/XXX.cfunction
list.c必须,列表
task.c必须,任务
queue.c可选,队列、信号量semaphore
event_groups.c可选,事件组等
time.c可选,software timer
croutine.c可选,过时了 协程

1.3.相关头文件

FreeRTOS本身的头文件:FreeRTOS/Source/include

移植时用到的头文件:FreeRTOS/Source/portable/[compiler]/[architecture] 含编译器和架构等

含有配置文件FreeRTOSConfig.h的目录

FreeRTOSConfig.h作用 比如任务配置,时间片和调度配置,内存分配等等,配置文件,比如选择调度算法:configUSE_PREEMPTION 每个demo都必定含有FreeRTOSConfig.h 建议去修改demo中的FreeRTOSConfig.h,而不是从头写一个

FreeRTOS.h使用task.h event_group.h semphr.h等事先要调用其头文件

1.4.内存管理

我们常说堆栈,要知道他的本质。

堆,heap,就是一块空闲的内存,需要提供管理函数,比如全局变量

  • malloc:从堆里划出一块空间给程序使用

  • free:用完后,再把它标记为"空闲"的,可以再次使用

栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中

  • 可以从堆中分配一块空间用作栈

后续聊到任务这个概念会根深蒂固。

文件在 FreeRTOS/Source/portable/MemMang 下,它也是放在 portable 目录下,表示你可以提供自己的函数。

源码中默认提供了5个文件,对应内存管理的5种方法。

heap_1.c 分配简单,时间确定 只分配、不回收

heap_2.c 动态分配、最佳匹配 碎片、时间不定

heap_3.c 调用标准库函数 速度慢、时间不定

heap_4.c 相邻空闲内存可合并 可解决碎片问题、时间不定

heap_5.c 在heap_4基础上支持分隔的内存块 可解决碎片问题、时间不定

详细刨析heap

1.4.1.Heap_1
  • 它只实现了pvPortMalloc,没有实现vPortFree

  • 如果你的程序不需要删除内核对象,那么可以使用heap_1

  • 实现最简单 没有碎片问题 一些要求非常严格的系统里,不允许使用动态内存,就可以使用heap_1

/* Allocate the memory for the heap. */
#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )
​
/* The application writer has already defined the array used for the RTOS
* heap - probably so it can be placed in a special segment or address. */
    extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
    static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif /* configAPPLICATION_ALLOCATED_HEAP */

实际上很简单,就是定义了一个大数组,每次从此当中分配空间。

FreeRTOS在创建任务时,需要2个内核对象:task control block(TCB)、stack。

这里补充一下

1)TCB任务控制块:TCB是FreeRTOS用来管理每个任务的数据结构,它存储了与任务相关的所有元数据。每个任务都有自己的TCB,用于管理该任务的状态、优先级等信息。

任务的状态:比如任务是就绪状态、阻塞状态、挂起状态等。

任务的优先级:任务的调度优先级,用来决定任务在什么时候运行。

任务的上下文:包括任务的程序计数器(PC)、寄存器状态等,保证任务切换时能恢复到正确的执行点。

任务的堆栈指针:指向该任务的堆栈,用于保存任务执行时的局部变量和函数调用信息。

任务的名字:如果启用了任务名字功能,TCB中会存储任务的名字。

任务的事件对象:用于与事件、信号量、队列等对象的交互。

其次正好聊聊TCB 不是句柄本身,任务句柄实际上是指向TCB的一个指针,用户通过任务句柄来操作和管理任务,比如暂停、删除、查询任务状态等。TCB 是保存任务信息的数据结构,而 任务句柄 是指向TCB的指针。

TaskHandle_t xTaskHandle;

2)Stack:堆栈是任务执行时的运行时存储空间,每个任务都有自己独立的堆栈,用来存储局部变量、函数调用返回地址、CPU寄存器的内容等。任务在执行过程中,堆栈会随着函数调用、返回等操作进行动态增长和缩减。

局部变量:任务执行时在函数内部声明的局部变量。

返回地址:函数调用返回的地址信息。

任务的CPU寄存器内容:任务在切换时保存的CPU寄存器信息,用来在任务恢复时继续执行。

综上,开始讲述heap1的使用过程,内存分配过程。

A:创建任务之前整个数组都是空闲的

B:创建第1个任务之后,蓝色区域被分配出去了

C:创建3个任务之后的数组使用情况

1.4.2.Heap_2(最佳匹配算法)

Heap_2之所以还保留,只是为了兼容以前的代码。新设计中不再推荐使用Heap_2。建议使用Heap_4 来替代Heap_2,更加高效。

Heap_2也是在数组上分配内存,跟Heap_1不一样的地方在于:

  • Heap_2使用最佳匹配算法(best fit)来分配内存

  • 它支持vPortFree(heap1不支持)

最佳匹配算法: 假设heap有3块空闲内存:5字节、25字节、100字节 pvPortMalloc想申请20字节 找出最小的、能满足pvPortMalloc的内存:25字节 把它划分为20字节、5字节 返回这20字节的地址 剩下的5字节仍然是空闲状态,留给后续的pvPortMalloc使用

与Heap_4相比,Heap_2不会合并相邻的空闲内存,所以Heap_2会导致严重的"碎片化"问题。

但是,如果申请、分配内存时大小总是相同的,这类场景下Heap_2没有碎片化的问题。所以它适合这 种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的(创建任务时,需要分配TCB和栈,TCB 总是一样的)。

虽然不再推荐使用heap_2,但是它的效率还是远高于malloc、free。

使用heap_2时,内存分配过程如下图所示

A:创建了3个任务

B:删除了一个任务,空闲内存有3部分:顶层的、被删除任务的TCB空间、被删除任务的Stack空 间

C:创建了一个新任务,因为TCB、栈大小跟前面被删除任务的TCB、栈大小一致,所以刚好分配 到原来的内存

1.4.3.Heap_3

Heap_3使用标准C库里的malloc、free函数,所以堆大小由链接器的配置决定,配置项 configTOTAL_HEAP_SIZE不再起作用。 C库里的malloc、free函数并非线程安全的,Heap_3中先暂停FreeRTOS的调度器,再去调用这些函数,使用这种方法实现了线程安全。

系统内存资源较大、需要使用标准C库动态分配的情况,但暂停调度器的方式可能会带来性能损耗,因为在暂停期间无法进行任务切换。

1.4.4.Heap_4(首次适应算法)

跟Heap_1、Heap_2一样,Heap_4也是使用大数组来分配内存。 Heap_4使用首次适应算法(first fit)来分配内存。它还会把相邻的空闲内存合并为一个更大的空闲内存, 这有助于较少内存的碎片问题。

首次适应算法:

  • 假设堆中有3块空闲内存:5字节、200字节、100字节

  • pvPortMalloc想申请20字节

  • 找出第1个能满足pvPortMalloc的内存:200字节

  • 把它划分为20字节、180字节

返回这20字节的地址

剩下的180字节仍然是空闲状态,留给后续的pvPortMalloc使用

Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景: 频繁地分配、释放不同大小的内存。

Heap_4的使用过程举例如下

A:创建了3个任务

B:删除了一个任务,空闲内存有2部分:

顶层的

被删除任务的TCB空间、被删除任务的Stack空间合并起来的

C:分配了一个Queue,从第1个空闲块中分配空间

D:分配了一个User数据,从Queue之后的空闲块中分配

E:释放的Queue,User前后都有一块空闲内存

F:释放了User数据,User前后的内存、User本身占据的内存,合并为一个大的空闲内存

所以回到这里,这是重点,Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用于这种场景: 频繁地分配、释放不同大小的内存。

1.4.5.Heap_5

Heap_5分配内存、释放内存的算法跟Heap_4是一样的。 相比于Heap_4,Heap_5并不局限于管理一个大数组:它可以管理多块、分隔开的内存。 在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用Heap_5。 既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:

  • 在使用pvPortMalloc之前,必须先指定内存块的信息

  • 使用vPortDefineHeapRegions来指定这些信息

怎么指定一块内存?使用如下结构体:

typedef struct HeapRegion
{
    uint8_t * pucStartAddress; // 起始地址
    size_t xSizeInBytes;       // 大小
} HeapRegion_t;

怎么指定多块内存?使用一个HeapRegion_t数组,在这个数组中,低地址在前、高地址在后。 比如:

HeapRegion_t xHeapRegions[] =
{
 { ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000
 { ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000
 { NULL, 0 } // 表示数组结束
 };

vPortDefineHeapRegions函数原型如下:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

把xHeapRegions数组传给vPortDefineHeapRegions函数,即可初始化Heap_5。

二、任务管理

2.1.什么是任务?任务的本质

从这个角度想:函数被暂停时,我们怎么保存它、保存什么?怎么恢复它、恢复什么?

任务是一个函数吗?

  • 函数保存在Flash上

  • Flash上的函数无需再次保存

  • 所以:任务不仅仅是函数

任务时变量吗?

  • 单纯通过变量无法做事

  • 所以:任务不仅仅是变量

任务是一个运行中的函数

  • 运行中:可以曾经运行,现在暂停了,但是未退出

  • 怎么描述一个运行中的函数

  • 假设在某一个瞬间时间停止,你怎么记录这个运行中的函数

要立即任务的本质,需要理解ARM架构、汇编

2.2.ARM架构

ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:

  • 对内存只有读、写指令

  • 对于数据的运算是在CPU内部实现

  • 使用RISC指令的CPU复杂度小一点,易于设计

比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:

细看这几个步骤,有些疑问:

  • 读a,那么a的值读出来后保存在CPU里面哪里?

  • 读b,那么b的值读出来后保存在CPU里面哪里?

  • a+b的结果又保存在哪里?

我们需要深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。

CPU运行时,先去Flash上取得指令,再执行指令:

  • 把内存a的值读入CPU寄存器R0

  • 把内存b的值读入CPU寄存器R1

  • 把R0、R1累加,存入R0

  • 把R0的值写入内存a

怎么理解Flash上的指令?看下一节。

2.3.汇编指令

  • 读内存:Load,LDR

  • 写内存:Store,STR

  • 加法:ADD

  • 入栈:PUSH,实质上就是写内存STR

  • 出栈:POP,实质上就是读内存LDR

要读内存:读内存哪个地址?读到的数据保存在哪里?读多少字节?

  • LDR R0, [R1, #0x00]

    • 源地址:R1+0x00,注意:不是读R1,是把R1的值当做内存的地址

    • 目的:R0,CPU的寄存器

    • 长度:4字节,LDR指令就是读4字节,LDRH是读2字节,LDRB是读1字节

要写内存:写内存哪个地址?从哪里得到数据?写多少字节?

  • STR R0, [R1, #0x00]

    • 目的地址:R1+0x00,注意:不是写R1,是把R1的值当做内存的地址

    • 源:R0,CPU的寄存器

    • 长度:4字节,STR指令就是读4字节,STRH是读2字节,STRB是读1字节

入栈:把CPU的寄存器的值,写到内存上

  • PUSH {R3, LR}

    • 源:CPU的寄存器R3、LR的值

    • 目的:内存,内存哪里?使用CPU的SP寄存器指定内存地址

    • 长度:大括号里所有寄存器的数据长度,每个寄存器4字节

    • 注意:低编号的寄存器,保存在内存的低地址处

    • 执行结果如下

出栈:把内存中的数值,写到CPU的寄存器

  • POP {R3, PC}

    • 源:内存,内存哪里?使用CPU的SP寄存器指定内存地址

    • 目的:CPU的寄存器R3、PC的值

    • 长度:大括号里所有寄存器的数据长度,每个寄存器4字节

    • 注意:内存的低地址处的数据,写到CPU低编号的寄存器

    • 执行结果如下

其他知识:

  • CPU内部有R0、R1、……、R15共16个寄存器

  • 某些寄存器有特殊作用

    • R13,别名SP,栈寄存器,保存着栈的地址

    • R14,别名LR,返回地址,保存着函数的返回地址

    • R15,别名PC,程序计数器,也就是当期程序运行到哪了

补充:SP、LR、PC

SP:堆栈指针,指向堆栈的顶部。

LR:链接寄存器,保存函数返回的地址。

PC:程序计数器,保存当前执行的指令地址。 这些寄存器在任务切换、函数调用和中断处理等操作中扮演着至关重要的角色。

2.4.怎么保存函数的现场

2.5.要保存什么 保存现场的几种场景 任务的重点

  • 程序运行到了哪里?PC寄存器的值

  • R2的值:我辛辛苦苦从内存里读到的值放在R2里,函数继续运行时,R2的值不要被破坏了

  • 只需要保存R2吗?切换任务的话,所有的寄存器都要保存

  • 保存在哪里?内存里! 内存哪里?栈里!

  • 函数调用 比如利用R1,R0这些用来传参的,就不用保存,不用全部保存,有些不用保存的。

  • 中断处理 对于F103,M3,M4,硬件保存一部分,软件保存一部分用到的寄存器。

  • 任务切换 全部寄存器都要保存,让CPU给另一个任务用嘛,不能破坏原本的东西。

2.6.创建任务

任务被切换的瞬间都保存在栈里,这是栈的本质,函数调用,局部变量都是。

堆:一块内存空间,可以从中分配一个小buffer,用完之后再放回去,全局变量

栈:也是一块内存空间,CPU的SP寄存器指向他,他可以用于函数调用,局部变量,多任务保存现场

void ATaskFunction( void *pvParameters );

这个函数不能返回 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数

函数内部,尽量使用局部变量: 每个任务都有自己的栈 每个任务运行这个函数时 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里

不同任务的局部变量,有自己的副本 函数使用全局变量、静态变量的话 只有一个副本:多个任务使用的是同一个副本 要防止冲突(后续会讲 引入队列等)

2.7.任务优先级和Tick

优先级的取值范围是:0~(configMAX_PRIORITIES – 1),数值越大优先级越高。

  • 确保高优先级的、可运行的任务,马上就能执行

  • 对于相同优先级的、可运行的任务,轮流执行

Tick:对于同优先级的任务,它们“轮流”执行。怎么轮流?你执行一会,我执行一会。

FreeRTOS中也有心跳,它使用定时器产生固定间隔的中断。这叫Tick、滴答,比如每10ms发生一次时 钟中断。

  • 假设t1、t2、t3发生时钟中断

  • 两次中断之间的时间被称为时间片(time slice、tick period)

  • 时间片的长度由configTICK_RATE_HZ 决定,假设configTICK_RATE_HZ为100,那么时间片长度就 是10ms

补充:例如陆续创建任务1以及任务2时并且优先级相同,他们的调度过程是任务2任务1任务2任务1任务2。。。

为什么会是任务2优先?因为创建的任务同一个优先级都会陆续放入一个链表当中,链表尾部先执行,再到头部。

怎么管理?

  • 怎么取出要运行的任务?

    • 找到最高优先级的运行态、就绪态任务,运行它

    • 如果大家平级,轮流执行:排队,链表前面的先运行,运行1个tick后乖乖地去链表尾部排队

谁进行调度?

  • TICK中断!

2.8.任务状态

任务状态转换图

以前我们很简单地把任务的状态分为2中:运行(Runing)、非运行(Not Running)。

对于非运行的状态,还可以继续细分,如下

Task3执行vTaskDelay后:处于非运行状态,要过3秒种才能再次运行

Task3运行期间,Task1、Task2也处于非运行状态,但是它们随时可以运行

这两种"非运行"状态就不一样,可以细分为:

  • 阻塞状态(Blocked)

  • 暂停状态(Suspended)

  • 就绪状态(Ready)

阻塞状态

在实际产品中,我们不会让一个任务一直运行,而是使用"事件驱动"的方法让它运行:

  • 任务要等待某个事件,事件发生后它才能运行

  • 在等待事件过程中,它不消耗CPU资源

  • 在等待事件的过程中,这个任务就处于阻塞状态(Blocked)

在阻塞状态的任务,它可以等待两种类型的事件:

  • 时间相关的事件

可以等待一段时间:我等2分钟

也可以一直等待,直到某个绝对时间:我等到下午3点

  • 同步事件:这事件由别的任务,或者是中断程序产生

例子1:任务A等待任务B给它发送数据

例子2:任务A等待用户按下按键

同步事件的来源有很多(这些概念在后面会细讲):

队列(queue)

二进制信号量(binary semaphores)

计数信号量(counting semaphores)

互斥量(mutexes)

递归互斥量、递归锁(recursive mutexes)

事件组(event groups)

任务通知(task notifications)

暂停状态 唯一的方法是通过vTaskSuspend函数

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

就绪状态

这个任务完全准备好了,随时可以运行:只是还轮不到它。这时,它就处于就绪态(Ready)。

三、两个Delay函数

vTaskDelay:至少等待指定个数的Tick Interrupt才能变为就绪状态

vTaskDelayUntil:等待到指定的绝对时刻,才能变为就绪态。

void vTaskDelay( const TickType_t xTicksToDelay ); /* xTicksToDelay: 等待多少给
Tick */
/* pxPreviousWakeTime: 上一次被唤醒的时间
 * xTimeIncrement: 要阻塞到(pxPreviousWakeTime + xTimeIncrement)
 * 单位都是Tick Count
 */
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
                            const TickType_t xTimeIncrement );

使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会 所以可以使用xTaskDelayUntil来让任务周期性地运行

四、空闲任务和钩子函数

除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部 分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运 行的任务:所以,我们要提供空闲任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器 时,这个函数内部会创建空闲任务:

  • 空闲任务优先级为0:它不能阻碍用户任务运行

  • 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞

空闲任务的优先级为0,这以为着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让 这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现 的。

要注意的是:如果使用 vTaskDelete() 来删除任务,那么你就要确保空闲任务有机会执行,否则就无 法释放被删除任务的内存。

我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环没执行一次,就会 调用一次钩子函数。钩子函数的作用有这些:

  • 执行一些低优先级的、后台的、需要连续执行的函数

  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任 务占据的时间,就可以算出处理器占用率。

  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式 了。

空闲任务的钩子函数的限制:

  • 不能导致空闲任务进入阻塞状态、暂停状态

  • 如果你会使用 vTaskDelete() 来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植 卡在钩子函数里的话,它就无法释放内存。

把这个宏定义为1:configUSE_IDLE_HOOK 实现 vApplicationIdleHook 函数

例如代码优先级为0 与空闲任务同级 调度的流程体现是;123;至于为什么?当没有空闲任务时就是按顺序执行,可以思考下,这是链表的思想。

空闲任务礼让:如果有同是优先级0的其他就绪任务,空闲任务主动放弃一次运行机会

五、分析任务中途放弃运行情况

比如中途主动放弃;等待;读队列 但是队列没有数据 只能等待,同步通信与互斥

比如中途被动放弃;位于Task1的中途的时刻,GPIO唤醒一个高优先级的任务;他会马上抢占,不管Task运行完美没有,任务优先级

任务切换;tick中断

六、同步通信与互斥_重点

可以把多任务系统当做一个团队,里面的每一个任务就相当于团队里的一个人。 团队成员之间要协调工作进度(同步)、争用会议室(互斥)、沟通(通信)。多任务系统中所涉及的概念,都 可以在现实生活中找到例子。

各类RTOS都会涉及这些概念:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)等。我们先站在更高角度来讲解这些概念。

什么叫同步?就是:哎哎哎,我正在用厕所,你等会。

什么叫互斥?就是:哎哎哎,我正在用厕所,你不能进来。

这里补充一下这里的互斥锁mutex,理论上来说是谁上锁谁打开,可是在这里是谁上锁任何人都可以打开,是工程师人为需要遵守的规则。

6.1.各类方法的对比

能实现同步、互斥的内核方法有:队列(queue)、互斥量(mutex)、信号量(samaphore)、事件组(event_group)、任务通知(task notification)

它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:

  • A获取资源,用完后A释放资源

  • A获取不到资源则阻塞,B释放资源并把A唤醒

  • A获取不到资源则阻塞,并定个闹钟;A要么超时返回,要么在这段时间内因为B释放资源而被唤 醒。

内核对象生产者消费者数据/状态说明
队列ALLALL数据:若干个数据 谁都可以往队列里扔数据谁都可以从队列里读数据用来传递数据 发送者、接收者无限制, 一个数据只能唤醒一个接收者
互斥锁A开锁A上锁位:0、1 我上锁:1变为0, 只能由我开锁:0变为1就像一个空厕所, 谁使用谁上锁, 也只能由他开锁
信号量ALLALL数量:0~n 谁都可以增加一个数量, 谁都可消耗一个数量用来维持资源的个数, 生产者、消费者无限制, 1个资源只能唤醒1个接收者
任务通知ALL只有我数据、状态都可以传输, 使用任务通知时, 必须指定接受者,句柄N对1的关系: 发送者无限制, 接收者只能是这个任务
事件组ALLALL多个位:或、与 谁都可以设置(生产)多个位, 谁都可以等待某个位、若干个位用来传递事件, 可以是N个事件, 发送者、接受者无限制, 可以唤醒多个接收者:像广播

使用图形对比如下:

队列:

  • 里面可以放任意数据,可以放多个数据

  • 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据

事件组:

  • 一个事件用一bit表示,1表示事件发生了,0表示事件没发生

  • 可以用来表示事件、事件的组合发生了,不能传递数据

  • 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒

信号量:

  • 核心是"计数值"

  • 任务、ISR释放信号量时让计数值加1

  • 任务、ISR获得信号量时,让计数值减1

任务通知:

  • 核心是任务的TCB里的数值

  • 会被覆盖

  • 发通知给谁?必须指定接收任务

  • 只能由接收任务本身获取该通知

互斥量:

  • 数值只有0或1

  • 谁获得互斥量,就必须由谁释放同一个互斥量

七、队列的核心

1)关中断

2)环形buffer

3)链表 -> 放数据 取数据

整个队列流程如下:

a.有中断关中断

b.有Data 读Data

没Data 返回 ERR

休眠

c.终于有Data了 copy Data

唤醒 Queue 拿数据移除任务 把它从DeayList -> ReadyList

一个任务写队列时,如果队列已经满了,它会被挂起,何时被唤醒?

超时:

  • 任务写队列不成功时,它会被挂起:从ready list移到delayed list中

  • 在delayed list中,按照"超时时间"排序

  • 系统Tick中断不断发生,在Tick中断里判断delayed list中的任务时间到没?时间到后就唤醒它

别的任务读队列:

八、信号量的核心

本质还是队列的套壳。

信号量就是特殊的队列。

队列里使用环形缓冲区存放数据,

信号量里只记录计数值。

九、互斥量的核心

互斥量就是特殊的队列。

互斥量更是特殊的信号量,

互斥量实现了优先级继承。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7yewh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值