当前位置: 首页 > news >正文

FreeRTOS学习笔记(五)任务进阶篇

文章目录

  • 前言
  • 一、列表和列表项
    • 1.1 xList 和 xLIST_ITEM
    • 1.2 相关API函数
    • 1.3 任务就绪列表
  • 二、任务调度器的启动过程
    • 2.1 PendSV 和 SysTick 寄存器
    • 2.2 prvStartFirstTask( )
    • 2.3 xPortStartScheduler( )
    • 2.4 vTaskStartScheduler( ) 的整体流程
  • 三、任务切换
    • 3.1基于 SysTick 中断的自动任务切换
      • 3.1.1 延时函数和上下文切换
      • 3.1.2 PendSV 的作用
      • 3.1.3 整体流程总结(自动)
    • 3.2 基于外部中断的任务切换
      • 3.2.1 外部中断触发任务切换的步骤
      • 3.2.2 portYIELD_FROM_ISR( )
  • 四、时间片调度
    • 4.1 时间片的基本特点
    • 4.2 时间片调度的基本原理
    • 4.3 时间片调度与临界区保护的关系


前言

  本节内容主要是对基础篇的补充,大部分内容是对vTaskStartScheduler()的各种函数底层的探究,如果不深入了解的朋友可以跳过了,这些寄存器或者底层函数通常情况freertos会自动帮我们调整或者调度。


一、列表和列表项

  在 FreeRTOS 中,列表(List) 和 列表项(List Item) 是用于链表管理的两个核心概念,它们负责调度任务、管理延迟任务等。列表是容器,用来存储多个列表项,而列表项表示任务或其他内核对象,通过链表的结构被链接到列表中。当调度器需要调度任务时,它会遍历就绪任务列表,通过链表项来找到下一个要执行的任务。实际上,FreeRTOS 中的列表是由链表构成的“双向链表”形式,而列表项也就是链表的节点。
在这里插入图片描述

1.1 xList 和 xLIST_ITEM

  列表(List)是 FreeRTOS 中的链表结构,用来管理任务或其他内核对象的集合。它包含多个xLIST_ITEM,即列表项。通过链表的结构,FreeRTOS 可以高效地管理和调度任务。xList 结构体定义如下所示:

struct xLIST
{volatile UBaseType_t uxNumberOfItems;    // 列表中当前的项目数ListItem_t *pxIndex;                     // 当前列表项的指针,用于遍历列表ListItem_t xListEnd;                     // 作为链表的末端标志,不存储实际任务
};
typedef struct xLIST List_t;
  • uxNumberOfItems:存储当前列表中的项目数量。
  • pxIndex:用于指向链表中的当前元素,调度器在遍历链表时使用它。
  • xListEnd:这是链表的末端标志节点,它并不存储实际任务,而是作为链表结束的标记。它的值通常被设置为极大值,以确保任何插入的节点都在它之前。

  xList_Item 是用于链表管理的一个节点(列表项)结构体,它是任务和其他内核对象(如队列、信号量等)在链表中存储时使用的基础数据结构。通过链表机制,xList_Item将各个任务或内核对象关联到相应的任务状态链表中(如就绪链表、延迟链表等)。在 FreeRTOS 的 list.h 文件中,xList_Item 结构体定义如下:

struct xLIST_ITEM
{TickType_t xItemValue;               // 用于排序的值,通常是时间戳或优先级struct xLIST_ITEM *pxNext;           // 指向链表中下一个节点的指针struct xLIST_ITEM *pxPrevious;       // 指向链表中前一个节点的指针void *pvOwner;                       // 指向该项所属对象(如任务、队列、信号量等)void *pvContainer;                   // 指向包含该项的链表
};
typedef struct xLIST_ITEM ListItem_t;
  • xItemValue:这是该节点的值,通常用于排序。例如,当任务处于延迟状态时,xItemValue 可能表示任务可以被唤醒的时间戳;在就绪列表中,xItemValue 则可能与任务的优先级相关。
  • pxNext:指向下一个链表节点的指针。链表的下一项是基于该指针来确定的。
  • pxPrevious:指向前一个链表节点的指针。通过它可以从当前节点回到前一个节点。
  • pvOwner:该指针指向这个链表项的拥有者,通常是指向任务控制块(TCB)的指针。通过它可以找到对应的任务或其他内核对象。
  • pvContainer:指向包含该节点的链表,这个指针表明该节点属于哪个链表结构。

  minixLIST_ITEM 是一种简化版的列表项结构体。与 xListItem 类似,minixLIST_ITEM 也是用于链表中的节点结构,只不过它的功能更简单,主要用于某些不需要完整列表管理功能的地方,通常用于内核级的优化。与 xListItem相比minixLIST_ITEM 没有 pvOwner 和 pvContainer 成员,因为它不需要知道链表项所属的任务控制块(TCB)或容器链表。这使得 minixLIST_ITEM 更加轻量,适用于不需要关联任务或特定对象的场景。xListItem 则是功能更全的链表节点,适合于任务调度、状态管理等场景。在 list.h 文件中,minixLIST_ITEM 结构体被定义为:

// 常用于列表末尾
struct xMINI_LIST_ITEM
{TickType_t xItemValue;                   // 用于排序或比较的值struct xLIST_ITEM *pxNext;               // 指向下一个列表项的指针struct xLIST_ITEM *pxPrevious;           // 指向前一个列表项的指针
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

1.2 相关API函数

函数描述
vListInitialise(List_t *pxList)初始化一个链表,并设置链表的末端标记,并将链表的初始长度设为 0
vListInitialiseItem(ListItem_t *pxItem)初始化一个链表项并清空链表项中的 pvContainer 指针,表示该项当前不属于任何链表
uxListRemove(ListItem_t *pxItem)从链表中删除指定的列表项。如果成功移除,返回链表中的剩余项数
vListInsert(List_t *pxList, ListItem_t *pxNewListItem)将一个新项插入到 xItemValue指定的链表中
vListInsertEnd(List_t *pxList, ListItem_t *pxNewListItem)将一个新项插入到链表的末端,不进行排序,常用于无序链表的操作
listGET_OWNER_OF_NEXT_ENTRY(ListItem_t *pxItem, List_t *pxList)返回链表中下一个列表项的拥有者,即该项所属的任务或内核对象。它也会更新 pxIndex,用于遍历整个链表
listGET_OWNER_OF_HEAD_ENTRY(List_t *pxList)获取链表中第一个项的拥有者,即该项所属的任务或内核对象
listCURRENT_LIST_LENGTH(List_t *pxList)返回指定链表中的列表项数量
listLIST_IS_EMPTY(List_t *pxList)检查指定链表是否为空

1.3 任务就绪列表

  在 FreeRTOS 中,任务就绪列表(Ready List)是用于管理所有处于“就绪”状态的任务,即那些已准备好运行且可以被调度器选择的任务。根据优先级的不同,每个任务都被存放在与其优先级相对应的就绪列表中。当任务处于“就绪”状态时,FreeRTOS 调度器会根据优先级在这些列表中选择最合适的任务进行执行。每个就绪列表本质上是一个链表,其中每个节点是 ListItem_t 结构体。每个节点与任务的 TCB(任务控制块)相关联,记录了任务的优先级、状态等信息。调度器通过这些链表项来管理任务的状态。这里需要知道几个数据结构:

  • xReadyTasksLists[ ]:存储多个就绪列表的数组,数组的每一项对应一个优先级的就绪任务列表
  • xTasksWaitingTermination:管理处于终止状态的任务列表
  • pxDelayedTaskList 和 pxOverflowDelayedTaskList:延迟任务列表,用于管理那些暂时不能运行的任务。

二、任务调度器的启动过程

  在基础篇,我们提到使用vTaskStartScheduler( )进行任务调度器的启动,这是因为vTaskStartScheduler() 是用于启动调度器的标准接口,它已经封装了调度器启动过程中所有需要的操作,包括调用 prvStartFirstTask() 启动第一个任务。

2.1 PendSV 和 SysTick 寄存器

  在前章中断管理的内容中,我们介绍过了这两个寄存器,他们主要是用来在任务切换和系统计时。在实际启动 FreeRTOS 任务调度器时,FreeRTOS 帮我们配置了 PendSV 和 SysTick 寄存器,这里我们可以简单了解一下:

  • PendSV 寄存器
    • 作用:PendSV 是 ARM Cortex-M 系列处理器中的一个系统中断,用于触发上下文切换。FreeRTOS 通过 PendSV 实现任务切换。当需要切换任务时,FreeRTOS 会将 PendSV 设置为挂起状态,然后在下一个合适的时间(例如,任务进入阻塞状态或时间片结束时),触发 PendSV 中断来执行任务切换。
    • 配置:FreeRTOS 的移植代码会自动配置 PendSV 中断优先级,你不需要手动修改该部分内容。调度器调用 portYIELD() 或者在 ISR 中通过 portYIELD_FROM_ISR() 来触发 PendSV 中断。
  • SysTick 寄存器
    • 作用:SysTick 是一个周期性定时器,用于生成 FreeRTOS 的系统节拍(tick)。每次 SysTick 中断发生时,FreeRTOS 会更新系统时钟,并检查是否需要执行任务调度。SysTick 的频率决定了任务切换和延迟任务的时间粒度,通常设置为 1 ms 或其他合适的周期。
    • 配置:在 xPortStartScheduler() 函数中,FreeRTOS 会配置 SysTick 定时器。你需要确保系统的时钟频率已正确设置,以便产生合适的 tick 频率。通常,移植层会根据你的系统时钟自动配置 SysTick 定时器的值。

2.2 prvStartFirstTask( )

  prvStartFirstTask( )是 FreeRTOS 内部的一个函数,它负责初始化启动前第一个任务的环境,主要是重新设置MSP指针。那么什么是MSP指针呢?程序在运行的过程中需要一定的栈空间来保存局部的一些信息,内核提供了两个栈空间:主堆栈指针(MSP,由os内核、异常服务例程及所需要特权访问的应用程序来使用)、进程堆栈指针(PSP,用于常规应用代码)。在FreeRtos中,中断内使用MSP,中断以外使用PSP。
  prvStartFirstTask( )在这个过程中主要确保第一个任务的上下文被正确加载到 CPU 并开始调度。实际上,在启动第一个任务之前,系统一般处于 “任务未调度” 状态,prvStartFirstTask( )开启调度后,系统开始按照调度算法执行任务。这是一个非常底层的函数,它通常由 xPortStartScheduler( )调用,而xPortStartScheduler( )是启动调度器的特定于硬件的实现部分。

2.3 xPortStartScheduler( )

  xPortStartScheduler( )是 FreeRTOS 中一个重要的函数,用于启动任务调度器。它是 FreeRTOS 内核的核心部分之一,负责开始任务的调度和管理。在启动任务调度器之前,xPortStartScheduler() 通常会进行一些硬件初始化,例如配置定时器中断,以便 FreeRTOS 可以进行时间切片和任务调度。之后它会启用调度器,使得 FreeRTOS 的任务管理功能开始运行,并根据任务的优先级和状态,决定哪个任务应该运行。一旦调度器启动,系统将进入任务调度模式,开始按照任务的优先级执行各个任务。值得一提的是,vTaskStartScheduler( )是 xPortStartScheduler( )的实际实现,通常情况下我们不直接使用xPortStartScheduler( )函数,而统一使用vTaskStartScheduler( ) 。
  除此之外,xPortStartScheduler( )工作流程大致为:

  1. 初始化堆栈和任务:在调用 xPortStartScheduler() 之前,系统中的任务和堆栈已经被初始化。此函数的调用标志着任务调度的开始。
  2. 配置系统定时器:FreeRTOS 使用系统定时器进行时间片轮转和任务调度。xPortStartScheduler() 将配置并启用该定时器。
  3. 开始调度:调度器启动后,它会根据任务的优先级和状态,决定当前哪个任务应该运行。
  4. 调度循环:调度器将不断地在任务之间进行切换,直到系统重置或调度器被停止。

2.4 vTaskStartScheduler( ) 的整体流程

  可以说vTaskStartScheduler( ) 包含了任务启动的所有工作,我们可以通过直接调用 vTaskStartScheduler() 启动调度器,之后不需要额外考虑底层的就绪列表、中断处理、任务切换等细节。具体来说一旦你调用了 vTaskStartScheduler(),FreeRTOS 的调度器就会开始运行,它会自动从优先级最高的就绪任务列表中选择任务执行,这意味着你不需要显式地去管理任务的切换或就绪列表。如果两个任务的优先级不同,FreeRTOS 将始终选择优先级高的任务运行。当高优先级任务进入阻塞状态(比如等待事件、延迟等),才会调度低优先级任务。如果任务具有相同的优先级,调度器会通过时间片轮转的方式切换任务。除此之外,vTaskStartScheduler( )会自动配置和启动系统时钟(SysTick)中断以及 PendSV 中断,这些负责触发任务切换。当任务切换发生时,FreeRTOS 会保存当前任务的上下文,并切换到下一个任务,着意味着你不需要手动处理中断。最后,当任务从创建状态变为就绪状态,或者从阻塞状态重新进入就绪状态时,FreeRTOS 内核会自动更新任务的状态并自动将其插入或移除就绪列表

  1. 初始化内核数据结构:就绪列表、延迟列表、空闲任务钩子等;
  2. 创建空闲任务:prvIdleTask( );
  3. (可选)如启用了软件定时器,创建定时器服务任务xTimerCreateTimerTask( );
  4. 检查系统是否至少有一个任务可以运行;
  5. 配置系统节拍定时器:vPortSetupTimerInterrupt( )配置 SysTick 定时器;
  6. (可选)若需要进行浮点运算,使能浮点运算单元(FPU),并配置FPCCR(浮点运算上下文控制寄存器);
  7. 启动调度器:xPortStartScheduler( )自动调用prvStartFirstTask( )开启第一个任务;

三、任务切换

3.1基于 SysTick 中断的自动任务切换

3.1.1 延时函数和上下文切换

  在基础篇,我们提到可以利用主动进入延时状态进入任务切换,那么这是为什么呢?实际上,延时函数会让当前任务进入阻塞状态,意味着该任务将不会再参与 CPU 的调度,直到设定的延时时间结束。通过调用 vTaskDelay(),任务会主动放弃 CPU 的使用权,允许调度器选择其他任务执行。FreeRTOS 调度器会在系统下一个时钟节拍(tick)触发时运行,查看是否有其他任务就绪。如果有,调度器会切换到下一个任务,从而实现上下文切换。而切换工作的实现流程其实与前文提到的PendSV有关。

3.1.2 PendSV 的作用

  之前提到过,PendSV是 ARM Cortex-M 系列处理器中专门用于任务上下文切换的中断,由于其可以被调度器异步触发,因此它可以等待处理器处理完高优先级的中断后再执行。故而我们通常将它的优先级设置为最低,以免打断其他中断的执行。当 FreeRTOS 的调度器判断需要切换任务时(如当前任务进入阻塞状态、延时时间到、或有高优先级任务就绪等情况),会决定要触发任务上下文切换。在任务切换时,调度器只会触发 PendSV 中断,实际的上下文切换操作(保存和恢复任务寄存器、切换栈指针等)则是在 PendSV 中断处理程序中完成的。
  其实简单来说,PendSV的触发流程有以下两种可能:

  • (自动)滴答定时器定期中断 -> 进行调度器检查 -> 需要进行上下文切换 -> 触发PendSV 中断
  • (手动)利用类似portYIELD()的API函数直接修改 ICSR 寄存器中的 PendSVSet 位SCB->ICSR = SCB_ICSR_PENDSVSET_Msk

3.1.3 整体流程总结(自动)

  1. 延时函数的调用(如 vTaskDelay()):延时任务主动放弃 CPU 控制权,并进入阻塞状态。
  2. 滴答定时器 SysTick:SysTick 产生系统节拍中断,定期更新任务的状态。
  3. 调度器检查:在 SysTick 中断中,调度器检查是否有任务需要切换,是否有延时到期的任务。
  4. 触发 PendSV:如果需要切换任务,调度器触发 PendSV 中断。
  5. 上下文切换:PendSV 中断处理程序执行实际的任务上下文切换,保存当前任务状态,恢复下一个任务状态。

3.2 基于外部中断的任务切换

  利用中断进行任务切换是一种通过硬件中断机制和RTOS调度器来强制任务切换的方式,这种方式通常用于响应外部事件,比如按键中断、通信中断等。

3.2.1 外部中断触发任务切换的步骤

  1. 外部中断发生:当某个外部事件(如按钮按下或数据到达)触发外部中断时,中断服务例程会被调用。
  2. 在中断中唤醒阻塞任务:如果某个任务因为等待某个外部事件(如数据输入)而进入阻塞状态,当外部中断发生时,任务可以从阻塞状态恢复到就绪状态。FreeRTOS 提供了一些API函数(例如,xTaskNotifyFromISR() 或 xQueueSendFromISR() ),可以在中断中调用这些函数来唤醒任务。
  3. 触发任务切换:在中断服务例程中,如果唤醒了更高优先级的任务,可以调用任务切换请求函数,比如 portYIELD_FROM_ISR() 来触发上下文切换,这会请求调度器在中断结束后执行任务切换。

3.2.2 portYIELD_FROM_ISR( )

  portYIELD_FROM_ISR() 的原理和 portYIELD( ) 类似,它用于在中断结束时请求任务切换,都是通过触发 PendSV 中断来进行任务上下文切换的。

#define portYIELD_FROM_ISR(x) if (x) SCB->ICSR = SCB_ICSR_PENDSVSET_Msk

四、时间片调度

  时间片调度(Round Robin Scheduling)是一种常见的任务调度算法,特别适合用于多任务操作系统(如 FreeRTOS)中,它的目标是为每个任务分配平等的 CPU 时间,让多个任务能够“轮流”执行。这里需要知道时间片的基本概念:每个任务被分配固定时长的 CPU 运行时间(可设置)。任务只能在时间片内运行,超过时间片后,系统强制切换到下一个任务。

4.1 时间片的基本特点

  时间片调度算法非常简单,易于实现,尤其适合实时操作系统中。它的核心特性是公平,它确保每个任务都有机会获得 CPU 资源,不会因为某个任务长时间运行而饿死其他任务。除此之外,还有以下的基本特性:

  • 抢占式:时间片调度通常是抢占式的,这意味着即使任务没有运行完,系统也会强制中断任务,切换到下一个任务。
  • 任务切换:任务切换的实现通过计时器(如 SysTick 定时器)触发中断,在中断中保存当前任务的上下文,并恢复下一个任
  • 效率问题:如果任务切换频繁,会增加上下文切换的开销,尤其是当任务数量较多时,频繁的任务切换会导致性能下降。
  • 不适合长时间任务:对于执行时间较长的任务,时间片调度可能无法有效处理,可能导致整体系统效率降低。

4.2 时间片调度的基本原理

  在 FreeRTOS 等实时操作系统中,时间片调度是基本的调度机制之一,特别是当多个同优先级任务同时就绪时,调度器会以时间片的方式公平地分配 CPU 资源。FreeRTOS 的调度器可以根据时间片进行切换,借助系统滴答定时器(SysTick),实现定时中断。每当时间片到期时,系统会触发 PendSV 中断进行上下文切换,从而完成任务之间的切换。时间片的长度(即任务能够运行的时间)是由系统时钟频率和滴答频率(tick rate)共同决定的,由 configTICK_RATE_HZ 来控制。configTICK_RATE_HZ 是 FreeRTOS 的一个配置宏,定义了每秒钟产生的滴答中断次数,也就是任务调度器的频率。例如,如果 configTICK_RATE_HZ 被设置为 1000,那么每 1ms 系统会产生一次滴答中断,表示每个时间片的默认长度为 1ms。

#define configTICK_RATE_HZ (1000) // 每秒 1000 次 tick,表示每个时间片为 1ms

  我们以图示为例,讲述一下时间片是怎么作用于任务切换的:
在这里插入图片描述

  1. 初始化:所有任务准备就绪,调度器会按照一定顺序排列任务队列。
  2. 任务执行:从队列中的第一个任务开始运行,任务在它的时间片内完成其操作。
  3. 任务切换:当一个任务的时间片用完后,调度器中断该任务的执行,并将 CPU 控制权切换到下一个任务。
  4. 临界保护:在到达Task3时,(不到一个时间片)进入阻塞,任务完成之后才结束阻塞并切换下一个任务
  5. 循环调度:当所有任务轮流执行后,调度器会重新回到第一个任务继续调度。

4.3 时间片调度与临界区保护的关系

  相信看完上面的例子,你可能会产生疑惑:如果一个任务进入临界区保护,在它退出临界区之前,确实不会发生任务切换,那么这不是和时间片调度机制存在矛盾吗?实际上,RTOS 的时间片轮转(time slicing)和优先级调度是一种公平调度机制,在多任务系统中,可以保证不同任务在一段时间内得到执行。然而,临界区保护只是在某些特定代码段中避免任务切换,这段代码之外仍然会遵循时间片轮转机制。所以,临界区只保护短暂的关键代码段,而并不是让任务完全占据 CPU。与此同时,临界区通常只持续很短的时间,用于保护对共享资源的访问。临界区的时间应尽量短,以减少对实时性的影响。当任务不处于临界区时,RTOS 的调度机制仍然可以正常工作,包括时间片轮转、任务优先级调度等。正确使用临界区可以避免竞态条件,而不会对整个系统的实时性产生太大影响。

免责声明:本文参考了网上公开资料,仅用于学习交流,若有错误或侵权请联系笔者。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • <Rust>egui学习之小部件(九):如何在窗口中添加下拉列表combobox部件?
  • 同城搭子怎么找?靠谱找搭子平台排行榜前十名测评
  • Typora调整图片大小:两种方式zoom或width/height
  • [数据集][目标检测]街头摊贩识别检测数据集VOC+YOLO格式758张1类别
  • 【干货分享】央企国企的群面、半结构面试复习方法和经验总结
  • Java网络编程入门
  • 【生日视频制作】保时捷车主提车交车仪式感AE模板修改文字软件生成器教程特效素材【AE模板】
  • 创建Hive表后,查看表结构发现中文注释乱码
  • 【spring】RuleOptions RecommendCtx
  • 面试—Linux
  • dpdk——数据平面开发套件
  • 【开源免费】基于SpringBoot+Vue.J大学生租房平台(JAVA毕业设计)
  • Unity Xcode方式接入sdk
  • HashMap中常用的函数
  • 828华为云征文 | 搭建云服务器Flexus X实例,开启简单上云第一步
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • Akka系列(七):Actor持久化之Akka persistence
  • echarts的各种常用效果展示
  • Java的Interrupt与线程中断
  • node-sass 安装卡在 node scripts/install.js 解决办法
  • spring cloud gateway 源码解析(4)跨域问题处理
  • vue总结
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 浮现式设计
  • 复杂数据处理
  • 欢迎参加第二届中国游戏开发者大会
  • 机器学习中为什么要做归一化normalization
  • 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 突破自己的技术思维
  • 由插件封装引出的一丢丢思考
  • 智能合约开发环境搭建及Hello World合约
  • Java性能优化之JVM GC(垃圾回收机制)
  • ​TypeScript都不会用,也敢说会前端?
  • ​力扣解法汇总946-验证栈序列
  • ​如何在iOS手机上查看应用日志
  • ​软考-高级-信息系统项目管理师教程 第四版【第23章-组织通用管理-思维导图】​
  • #define、const、typedef的差别
  • #微信小程序(布局、渲染层基础知识)
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • (70min)字节暑假实习二面(已挂)
  • (C++20) consteval立即函数
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (笔试题)分解质因式
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (附源码)ssm基于web技术的医务志愿者管理系统 毕业设计 100910
  • (附源码)ssm捐赠救助系统 毕业设计 060945
  • (论文阅读23/100)Hierarchical Convolutional Features for Visual Tracking
  • (每日持续更新)jdk api之FileReader基础、应用、实战
  • (实测可用)(3)Git的使用——RT Thread Stdio添加的软件包,github与gitee冲突造成无法上传文件到gitee
  • (四)Controller接口控制器详解(三)
  • (四)库存超卖案例实战——优化redis分布式锁
  • (学习日记)2024.04.10:UCOSIII第三十八节:事件实验