外贸建站 厦门,河南住房和建设厅网站,html5网站模板 医院,国外做网站#x1f431;作者#xff1a;一只大喵咪1201 #x1f431;专栏#xff1a;《RTOS学习》 #x1f525;格言#xff1a;你只管努力#xff0c;剩下的交给时间#xff01; 同步与互斥 | 队列 #x1f349;同步与互斥#x1f366;同步#x1f366;互斥 #x1f349;队… 作者一只大喵咪1201 专栏《RTOS学习》 格言你只管努力剩下的交给时间 同步与互斥 | 队列 同步与互斥同步互斥 队列环形缓冲区读写任务链表操作队列的函数使用队列 总结 同步与互斥
FreeRTOS是一个实时操作系统是一个多任务系统任务之间存在同步关系如协调工作进度(同步)还有互斥关系就像争用会议室(互斥)。
到底什么是同步什么是互斥呢拿一个生活中上厕所的例子来说“等我用完厕所你再用厕所”。
同步我正在用厕所你得等会儿再用。互斥我正在用厕所你不能进来。
只看我和你必须得等我用完厕所你才能用我和你之间有必然的先后顺序存在协同关系这就是同步。
只看厕所同一时刻只能进去一个人我和你之间就是竞争关系这就是互斥。
同步
举一个同步关系的代码例子有两个任务任务1进行计算任务2打印计算结果但是必须等任务1计算完成才能打印这是同步关系。
第一种方式 如上图创建两个任务优先级都是1在任务1中进行5000000次加法运算完成后将完成标志位flagCalcEnd置一并且自杀(为了方便实验)。
在任务2中不断检测计算完成标志位flagCalcEnd当变成1以后打印五百万次求和的结果。 如上图结果所示在耗时3秒钟左右时计算完成标志位从0变成了1说明此时计算完成而且通过串口打印出了运算结果。
在这个计算过程中任务1和任务2按照时间片轮转的方式在执行任务1在进行计算任务2只是判断一下falgCalcEnd是否为1但是它执行的时间和任务1是一样的都是一个时间片。 此时计算完成耗时3秒左右。 第二种方式 如上图先只创建任务1然后进行计算当计算完成以后在任务1中创建任务2用来打印计算结果并且将flagCalcEnd标志位置一。 如上图这次运行结果和前面相比其他都相同只有耗费时间不同变成了1.5秒左右缩短了一倍。 此时计算完成耗时1.5秒左右。 虽然上面两种方式都能实现同步的目的但是显然第二种方式效率更高也就是在任务1进行计算的时候任务2应该处于阻塞状态不应该和任务1竞争CPU资源。
当任务1计算完成以后唤醒任务2来打印结果即可这样的同步关系才是高效合理的。
但是第二种方式在代码结构上就让任务1和任务2在串行执行而在FreeRTOS中多任务之间应该“并行”执行所以还是要用第一种方式但是要让任务2在任务1计算期间处于阻塞状态把CPU资源完全让出来。
该用什么样的方式实现同步呢后面本喵详细讲解。
互斥
再举一个互斥关系的例子两个任务使用一个串口打印信息这两个串口就是互斥关系。 如上图创建一个函数TaskGenericFunction里面使用串口打印启动任务时传入的参数创建两个新任务都调用这一个函数任务的优先级都是1两个任务打印各自的运行信息。 如上图此时看到的结果中打印出来的数据非常混乱一句话中既有属于任务3打印的内容也有任务4打印的内容这是因为两个任务没有互斥的使用串口。 如上图增加一个串口使用标志flagUARTused只有标志位是0的时候任务才能去使用串口在使用之前将标志位置一表示有任务在使用另一个任务就无法使用了。
使用完毕以后再将标志位清0另一个任务才可以使用此时就实现了两个任务之间的互斥。 如上图此时任务3和任务4打印出的信息就是独立的没有混杂在一起主要就是因为两个任务之间实现了互斥。 每个任务使用完串口以后主动延时一个Tcik这是为了削弱当前任务的竞争力。 如果不延时的话当前任务使用完串口将标志位置0后它仍然在占用CPU资源另一个任务还没有被调度当前任务就再次将标志位置一使用串口了这样就会导致任务4一直在使用串口有兴趣的小伙伴自己实验一下本喵就不演示了。 上面的方式真的实现互斥了吗 如上图考虑一个极端一点的情况任务3完成if判断以后但是还没有将标志位置一如上图红色线条所在位置此时任务3被切走了。
CPU开始执行任务4了由于此时标志位并没有被置1所以任务4也可以使用串口此时任务3和任务4的互斥关系就不存在了它两都可以使用串口。 这样的方式运行时间长了以后上面的这种情况就会成为必然。 除此之外一个任务在使用串口的时候另一个任务也会被调度但是它只是检测一下标志位是否为0和同步例子中的问题一样也会浪费CPU资源。
所以在一个任务使用串口的时候另一个任务也应该处于阻塞状态当前任务使用完毕以后唤醒另一个任务。
到底该用什么样的方式实现互斥呢后面本喵详细讲解。 同步与互斥经常放在一起讲是因为它们之间的关系很大“互斥”操作可以使用“同步”来实现我“等”你用完厕所我再用厕所。
串口例子中一个任务在使用串口另一个任务必须等当前任务使用完毕后才能使用。 这就是用“同步”来实现“互斥”。 队列
FreeRTOS有一套实现同步与互斥的方案同时也解决了前面本喵所说的问题第一种方案就是使用队列结构 如上图所示队列模型这是一个先进先出的结构左边的任务A和任务B是生产者也可以是更多任务生产者负责生成数据。
右边的任务C和任务D是消费者也可以是更多任务消费者负责消费(读取)队列中的数据。 只要队列中有空位生产者就可以生成数据。只要队列中有数据消费者就可以消费数据。生产任务和消费任务访问队列时所有任务之间都是互斥的只能有一个任务访问队列。 队列是如何实现同步与互斥的呢接下来就来看一下它的结构 如上图代码所示在queue.c中定义了一个队列结构体类型重命名为xQUEUE它有一个环形缓冲区还有两个链表一个存放生产者任务的TCB节点一个存放消费者任务的TCB节点还有队列长度以及队列中每个数据的大小等属性。
环形缓冲区
写 如上图所示队列中的环形缓冲区是通过指针pcHead和pcWriteTo来维护的所以这两个指针就代表着环形缓冲区。
当队列刚创建的时候环形缓冲区是空的假设创建的队列长度是N队列中每个元素的大小是sizeof(Item)头指针pcHead和写指针pcWiteTo指向环形队列的头部。
头指针pcHead永远指向头部不发生变化当有任务要向环形队列中写入数据时写入pcWriteTo指针指向的位置然后pcWriteTo指针向后移动指向下一个要写入的位置。
当向队列中写入第N个数据时pcWriteTo指针就会通过取模运算重新指向pcHead指向的队列头部向该位置写入数据。 读 如上图所示读取队列时通过读指针pcReadFrom来控制读取的位置但是从队列的结构体中并没有看到读指针pcReadFrom因为它在联合体union u中 如上图在队列中的联合体成员中QueuePointers_t成员中存在读指针pcReadFrom成员变量。
和pcWriteTo不同的是读指针pcReadFrom的起始位置在下标为N-1处读指针pcReadFrom指向的是上一次读取数据的位置。
有任务来读取数据时pcReadFrom先向后移动如果移动前指向最后一个位置那么同样通过取模运行指向环形队列头部然后将该位置的数据取出。 有了读指针和写指针后就可以判断环形缓冲区是否满了在写数据时当pcWriteTo pcReadFrom的时候就说明队列满了不能继续写数据了。
在读数据时当pcReadFrom pcWriteTo的时候说明队列空了无法继续读取数据了。
读写任务链表
队列有满或者空的情况此时FreeRTOS是怎么处理生产者任务或者消费者任务的呢 如上图在队列结构体中有两个链表分别是写任务链表xTaskWaitingToSend和读任务链表xTaskWaitingToReceive这是两个阻塞链表管理处于阻塞状态的读写任务。
当生产者任务向队列中写数据时发现pcWriteTopcReadFromFreeRTOS就将这个生产者任务设置成阻塞状态并且放入xTaskWaitingToSend链表中当队列中有空闲位置了就从xTaskWaitingToSend中唤醒一个任务向队列中写数据
当消费者任务从队列中读取数据时发现pcReadFrompcWriteToFreeRTOS就将这个消费者任务设置成阻塞状态并放入xTaskWaitingToReceive阻塞链表中当队列中有数据了就从xTaskWaitingToReceive唤醒一个任务从队列中读数据。
此时就做到了生产者任务和消费者任务之间的同步。 如上图生产者任务向队列中写数据时需要调用xQueueSend函数该函数内部会先将所有中断都关闭此时Tcik就无法产生中断也就不会调度其他任务这段时间CPU就只能执行这个任务数据写入完毕后再打开所有中断恢复Tick对其他任务的调度。
消费者任务从队列中读取任务时也一样也会先关闭所有中断禁止调度其他任务数据读取完毕后再打开中断恢复调度本喵这里就不给大家看源码了。
此时就做到了生产者任务和消费者任务之间的互斥同一时刻只能有一个任务来访问队列。 既然读取和写入数据的任务个数没有限制那么当多个任务读取空队列或者多个任务向满队列中写入数据时这些任务都会进入阻塞状态此时就有多个任务在同一个链表中。
当队列中有数据时或者有空位时哪个任务会进入就绪态状态
优先级最高的任务如果大家优先级相同那么等待时间最久的任务会进入就绪状态。
操作队列的函数
使用队列的流程创建队列、写队列、读队列、删除队列。
创建
动态分配内存队列的内存在函数内部使用malloc动态分配。
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize );uxQueueLength队列长度最多能存放多少个数据(Item)。uxItemSize每个数据(Item)的大小以字节为单位。返回值成功返回队列句柄失败返回NULL一般都是因为内存不足。 静态分配内存队列的内存要用户事先分配好。
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,UBaseType_t uxItemSize,uint8_t *pucQueueStorageBuffer,StaticQueue_t *pxQueueBuffer);uxQueueLength队列长度最多能存放多少个数据(Item)。uxItemSize每个数据(Item)的大小以字节为单位。pucQueueStorageBuffer如果uxItemSize非0pucQueueStorageBuffer必须指向一个uint8_t数组此数组大小至少为uxQueueLength * uxItemSize。pxQueueBuffer必须创建一个StaticQueue_t结构体用来保存队列的数据结构。返回值成功返回队列句柄失败返回NULL一般都是因为内存不足。 写队列
/* 向队列尾部写入数据 */
BaseType_t xQueueSend(QueueHandle_t xQueue,const void *pvItemToQueue,TickType_t xTicksToWait);
/* 向队列尾部写入数据 */
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,const void *pvItemToQueue,TickType_t xTicksToWait);虽然这两个函数名称不一样但是作用是一样的都是向队列尾部写入数据。
/* 往队列头部写入数据 */
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,const void *pvItemToQueue,TickType_t xTicksToWait);这些函数用到的参数都是相同的 xQueue队列句柄要写哪个队列。pvItemToQueue数据指针这个数据会被拷贝到队列中。xTicksToWait如果队列满则无法写入新数据可以让任务进入阻塞状态xTicksToWait表示阻塞的最大时间(Tick Count)。如果被设为0无法写入数据时函数会立刻返回如果被设为portMAX_DELAY则会一直阻塞直到有空间可写。返回值pdPASS从队列读出数据入errQUEUE_EMPTY读取失败因为队列空了。 写数据时是将用户传入指针所指数据拷贝到队列中所以必须传指针。
读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait );xQueue队列句柄要读哪个队列。pvBufferbufer指针队列的数据会被复制到这个buffer。xTicksToWait 等待时间和写队列时一样。返回值pdPASS从队列读出数据入errQUEUE_EMPTY读取失败因为队列空了。 删除
void vQueueDelete( QueueHandle_t xQueue );只能删除使用动态方法创建的队列它会释放内存。
复位
BaseType_t xQueueReset( QueueHandle_t pxQueue);队列刚被创建时里面没有数据使用过程中可以调用该函数把队列恢复为初始状态此函的返回值是pdPASS必定复位成功。
查询
/* 返回队列中可用数据的个数 */
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );/* 返回队列中可用空间的个数 */
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );覆盖
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,const void * pvItemToQueue);当队列长度为1时可以使用该函数来覆盖数据。注意队列长度必须为1。当队列满时该函数会覆盖里面的数据这也意味着该函数不会被阻塞。
偷看
BaseType_t xQueuePeek(QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait);如果想让队列中的数据供多方读取也就是说读取时不要移除数据要留给后来人。那么可以使用该函数来窥视该函数会从队列中复制出数据但是不移除数据。这也意味着如果队列中没有数据那么偷看时会导致阻塞一旦队列中有数据以后每次偷看都会成功。
后面几个函数的参数和前面几个一样本喵没有再介绍照猫画虎即可。
使用队列
基本使用
现在来解决开篇时同步例子中的缺陷 如上图创建一个队列使用一个全局队列句柄xQueueCalcHandle来接收返回值再创建两个任务优先级都是1。 如上图任务1进行五百万次运算计算完成后将结果放入队列中任务2从队列中读取任务1的计算结果。 如上图可以看到整个计算过程花费1.5秒左右和我们前面串行方式耗时一样但是此时是同时创建的两个任务两个任务在并行执行。 读取数据的任务在队列中没有数据时处于阻塞状态不会竞争CPU资源当队列中有数据时才会唤醒它。 分辨数据源
当有多个发送任务通过同一个队列发出数据接收任务如何分辨数据来源数据本身带有来源信息比如写入队列的数据是一个结构体结构体中的IDataSouceID用来表示数据来源
typedef struct {ID_t eDataID;int32_t lDataValue;
}Data_t;不同的发送任务先构造好结构体填入自己的 eDataID再写队列接收任务读出数据后根据eDataID就可以知道数据来源了如下图所示
CAN任务发送的数据eDataIDeMotorSpeedHMI任务发送的数据eDataIDeSpeedSetPoint 如上图CAN任务和HMI任务将结构体数据写入到队列中后Controller任务在读取到数据时就可以通过结构体中的eDataID成员知道是哪个任务发送来的数据。 如上图枚举数据来源定义传输数据的结构体将两个发送任务要发送的数据放在一个数组中并初始化。 如上图创建俩个任务用来发送数据分别是CAN Task和HMI Task优先级都是2再创建一个接收数据的任务ReceiverTask优先级是1只有两个发送任务将队列写满阻塞后接收任务才能从队列中读取任务。 如上图所示两个发送任务分别发送自己的结构体数据到队列中接收任务从队列中读取结构体数据通过eDataID成员确定数据源并且打印数据。 如上图运行结果所示两个发送任务的优先级都是2所以它两先执行又因为HMI任务是后创建的所以先运行瞬间就将队列写满了这个过程中CAN任务还没有来得及被调度就因为队列满而被阻塞了。
接收任务开始读取后先读取的是队列中的HMI任务的数据此时队列出现空位所以唤醒了CAN任务写队列该数据排在最后有空位后再唤醒HMI任务如此往复。
所以读取的前五个都是HMI任务写的数据之后就是CAN任务和HMI任务写的数据交替出现。
传输大块数据
FreeRTOS的队列使用拷贝传输也就是要传输uint32_t时把4字节的数据拷贝进队列要传输一个8字节的结构体时把8字节的数据拷贝进队列。
如果要传输1000字节的结构体呢写队列时拷贝1000字节读队列时再拷贝1000字节先不说浪不浪费内存效率必然会很低
这时候我们要传输的是这个巨大结构体的地址或者是一个字符串的地址把它的地址写入队列对方从队列得到这个地址使用地址去访问那1000字节的数据。
使用地址来间接传输数据时这些数据放在RAM里对于这块RAM要保证这几点
RAM的所有者、操作者必须清晰明了。 这块内存就被称为共享内存。要确保不能同时修改RAM。比如在写队列之前只能由发送者修改这块RAM在读队列之后只能由接收者访问这块RAM。 RAM要保持可用 这块RAM应该是全局变量或者是动态分配的内存。对于动然分配的内存要确保它不能提前释放要等到接收者用完后再释放。另外不能是局部变量。 如上图代码创建一个队列长度为1元素大小是一个char*类型指针变量再创建一个写队列任务优先级是1再创建一个读队列任务优先级是2。 如上图发送任务中将指向字符串指针的地址(二级指针)写入到队列中读取任务中从队列的二级指针中将字符串地址拷贝到buffer中。 队列中存放的并不是整个字符串而是指向字符串的指针。 如上图读取任务成功从队列中拿到了字符串所在的地址通过该指针找到了字符串并且打印出来。 这个程序故意设置接收任务的优先级更高在它访问数组的过程中发送任务无法执行、无法写这个数组从而保证这个数组中数据的安全使得接收任务读取到的肯定是正确的值不会发生“读取到一半被切换下去让写队列任务向队列中写数据”的情况。
邮箱
FreeRTOS的邮箱概念跟别的RTOS不一样这里的邮箱称为橱窗也许更恰当
它是一个队列队列长度只有1。写邮箱新数据覆盖旧数据在任务中使用xQueueOverwrite() 既然是覆盖那么无论邮箱中是否有数据该函数总能成功写入数据。读邮箱读数据时数据不会被移除在任务中使用xQueuePeek()这意味着第一次调用时会因为无数据而阻塞一旦曾经写入数据以后读邮箱时总能成功。 如上图代码main函数就不贴图了创建了写队列任务优先级是2创建了读队列任务优先级是1。在写队列任务中先延时5个Tick然后再覆盖式的向队列中写入数据。
在读队列任务中使用xQueuePeek窥视队列中的数据不延时偷看成功则打印数据不成功则打印不能接收数据的字符串。 如上图所示写队列任务先开始执行一上来就延时进入阻塞状态此时队列中还没有数据所以此时正在运行的读队列任务无法从队列中读取数据所以打印无法接收数据的字符串。
5个Tick之后写队列任务被唤醒后抢占读队列任务向队列中写入数据然后再次进入延时此时队列中已经有数据了所以读取队列的任务可以从队列中偷看到数据由于写数据任务处于阻塞状态所以在一段时间内都没有覆盖数据所以读到的数据是相同的。
5个Tick之后写队列任务又被唤醒用新数据覆盖队列然后再进入延时阻塞状态读队列任务读取数据如此反复。 除了第一次读队列任务每次都能偷看到数据无论是否写入新的数据。 总结
要理解同步和互斥的概念认识到同步和互斥是相互依赖的。要明白队列是如果实现同步和互斥的大概清楚队列的运行机制。除此之外还要掌握不同情况下队列的使用。