3.5. 列表
任务之后,最常用的FreeRTOS数据结构是列表。FreeRTOS使用列表结构来跟踪调度任务,并执行队列。

图3.3:就绪列表全貌
这个FreeRTOS的列表是一个有着几个有趣的补充的标准循环双链表。下面就是列表元素:
struct xLIST_ITEM
{
portTickType xItemValue; /* The value being listed. In most cases
this is used to sort the list in
descending order. */
volatile struct xLIST_ITEM * pxNext; /* Pointer to the next xListItem in the
list. */
volatile struct xLIST_ITEM * pxPrevious; /* Pointer to the previous xListItem in
the list. */
void * pvOwner; /* Pointer to the object (normally a TCB)
that contains the list item. There is
therefore a two-way link between the
object containing the list item and
the list item itself. */
void * pvContainer; /* Pointer to the list in which this list
item is placed (if any). */
};
每个元素持有一个数字,xItemValue,这通常是一个被跟踪的任务优先级或者是一个调度事件的计时器值。列表保存从高到低的优先级指令,这意味着最高的优先级xItemValue(最大数)在列表的最前端,而最低的优先级xItemValue(最小数)在列表的末尾。
pxNext和pxPrevious指针是标准链表指针。pvOwner 列表元素所有者的指针。这通常是任务的TCB对象的指针。pvOwner被用来在vTaskSwitchContext()中加快任务切换:当最高优先级任务元素在pxReadyTasksLists[]中被发现,这个列表元素的pvOwner指针直接连接到需要任务调度的TCB。
pvContainer指向自己所在的这个列表。若列表项处于一个特定列表它被用作快速终止。任意列表元素可以被置于一个列表,如下所定义:
typedef struct xLIST
{
volatile unsigned portBASE_TYPE uxNumberOfItems;
volatile xListItem * pxIndex; /* Used to walk through the list. Points to
the last item returned by a call to
pvListGetOwnerOfNextEntry (). */
volatile xMiniListItem xListEnd; /* List item that contains the maximum
possible item value, meaning it is always
at the end of the list and is therefore
used as a marker. */
} xList;
列表的大小任何时候都是被存储在uxNumberOfItems中,用于快速列表大小操作。所有的列表都被初始化为容纳一个元素:xListEnd元素。xListEnd.xItemValue是一个定点值,当portTickType是16位数时,它等于xItemValue变量的最大值:0xffff,portTickType是32位数时为0xffffffff。其他的列表元素也可以使用相同的值;插入算法保证了xListEnd总是列表项中最后一个值。
自列表从高到低排序后,xListEnd被用作列表开始的记号。并且,自循环开始,xListEnd也被用作列表结束的记号。
你也许可以用一个单独的for()循环或者是函数调用来访问大多数“传统的”列表,去做所有的工作,就像这样:
for (listPtr = listStart; listPtr != NULL; listPtr = listPtr->next) {
// Do something with listPtr here...
}
FreeRTOS经常需要通过多个for()和while()循环,也包括函数调用来访问列表,因此它使用操纵pxIndex指针的列表函数来遍历这个列表。这个列表函数listGET_OWNER_OF_NEXT_ENTRY()执行pxIndex = pxIndex->pxNext;并且返回pxIndex。(当然它也会正确检测列尾环绕。)这种,当执行遍历的时候使用pxIndex,由列表自己负责跟踪“在哪里”的方法,使FreeRTOS可以休息而不用关心这方面的事。

图3.4:系统节拍计时器下的FreeRTOS就绪列表全貌
pxReadyTasksLists[]列出了在vTaskSwitchContext()中已经操纵完成的内容,是如何使用pxIndex的一个很好的例子。让我们假设我们仅有一个优先级,优先级0,并且有三个任务在此优先级上。这与我们之前看到的基本就绪列表图相似,但这一次我们将包括所有的数据结构和字段。
就如你在图3.3中所见,pxCurrentTCB显示我们当前正在运行任务B。下一个时刻,vTaskSwitchContext()运行,它调用listGET_OWNER_OF_NEXT_ENTRY()载入下一个任务来运行。如图3.4所示,这个函数使用pxIndex->pxNext找出下一个任务是任务C,并且pxIndex指向任务C的列表元素,同时pxCurrentTCB指向任务C的TCB。
请注意,每个struct xlistitem对象实际上都是来自相关TCB的xGenericListItem对象。
3.6. 队列
FreeRTOS允许任务使用队列来互相间通信和同步。中断服务程序(ISRs)同样使用队列来通信和同步。
基本队列数据结构如下:
typedef struct QueueDefinition
{
signed char *pcHead; /* Points to the beginning of the queue
storage area. */
signed char *pcTail; /* Points to the byte at the end of the
queue storage area. One more byte is
allocated than necessary to store the
queue items; this is used as a marker. */
signed char *pcWriteTo; /* Points to the free next place in the
storage area. */
signed char *pcReadFrom; /* Points to the last place that a queued
item was read from. */
xList xTasksWaitingToSend; /* List of tasks that are blocked waiting
to post onto this queue. Stored in
priority order. */
xList xTasksWaitingToReceive; /* List of tasks that are blocked waiting
to read from this queue. Stored in
priority order. */
volatile unsigned portBASE_TYPE uxMessagesWaiting; /* The number of items currently
in the queue. */
unsigned portBASE_TYPE uxLength; /* The length of the queue
defined as the number of
items it will hold, not the
number of bytes. */
unsigned portBASE_TYPE uxItemSize; /* The size of each items that
the queue will hold. */
} xQUEUE;
这是一个颇为标准的队列,不但包括了头部和尾部指针,而且指针指向我们刚刚读过或者写过的位置。
当刚刚创建一个队列,用户指定了队列的长度和需要队列跟踪的项目大小。pcHead和pcTail被用来跟踪队列的内部存储器。加入一个项目到队列就对队列内部存储器进行一次深拷贝。
FreeRTOS用深拷贝替代在项目中存放一个指针是因为有可能项目插入的生命周期要比队列的生命周期短。例如,试想一个简单的整数队列使用局部变量,跨几个函数调用的插入和删除。如果这个队列在局部变量里存储这些整数的指针,当整数的局部变量离开作用域时指针将会失效,同时局部变量的存储空间将被新的数值使用。
什么需要用户选择使用队列。若内容很少,用户可以把复制的内容进行排列,就像上图中简单整数的例子,或者,若内容很多,用户可以排列内容的指针。请注意,在这两种情况下FreeRTOS都是在做深拷贝:如果用户选择排列复制的内容,那么这个队列存储了每项内容的一份深拷贝;如果用户选择了排列指针,队列存储了指针的一份深拷贝。当然,用户在队列里存储了指针,那么用户有责任管理与内存相关的指针。队列并不关心你存储了什么样的数据,它只需要知道数据的大小。
FreeRTOS支持阻塞和非阻塞队列的插入和移除。非阻塞队列操作会立即返回"队列的插入是否完成?"或者 "队列的移除是否完成?"的状态。阻塞操作则根据特定的超时。一个任务可以无限期地阻塞或者在有限时间里阻塞。
一个阻塞任务——叫它任务A——将保持阻塞只要它的插入/移除操作没有完成,并且它的超时(如果存在)没有过期。如果一个中断或者另一个任务编辑了这个队列以便任务A的操作能够完成,任务A将被解除阻塞。如果此时任务A的队列操作仍然是允许的,那么它实际上会执行操作,于是任务A会完成它的队列操作,并且返回“成功”的状态。不过,任务A正在执行的那个时间,有可能同时有一个高优先级任务或者中断也在同一个队列上执行另一个操作,这会阻止任务A正在执行的操作。在这种情况下任务A将检查它的超时,同时,如果它未超时就恢复阻塞,否则就返回队列操作“失败”的状态。
特别需要注意的是,当任务被阻塞在一个队列时,系统保持运行所带来的风险;以及当任务被阻塞在一个队列时,有其他任务或中断在继续运行。这种阻塞任务的方法能不浪费CPU周期,使其他任务和中断可以有效地使用CPU周期。
FreeRTOS使用xTasksWaitingToSend列表来保持对正阻塞在插入队列里的任务的跟踪。每当有一个元素被移出队列,xTasksWaitingToSend列表就会被检查。如果有个任务在那个列表中等待,那个是未阻塞任务。同样的,xTasksWaitingToReceive保持对那些正阻塞在移除队列里的任务的跟踪。每当有一个新元素被插入到队列,xTasksWaitingToReceive列表就会被检查。如果有个任务在那个列表中等待,那个是未阻塞任务。
信号灯和互斥
FreeRTOS使用它的队列与任务通信,也在任务间通信。FreeRTOS也使用它的队列来实现信号灯与互斥。
有什么区别?
信号灯与互斥听上去像一回事,但它们不是。FreeRTOS同样地实现了它们,但本来它们以不同的方式被使用。它们是如何不同地被使用?嵌入式系统宗师Michael Barr说这是在他文章中写得最好的,“信号灯与互斥揭秘”:
The correct use of a semaphore is for signaling from one task to another. A mutex is meant to be taken and released, always in that order, by each task that uses the shared resource it protects. By contrast, tasks that use semaphores either signal ["send" in FreeRTOS terms] or wait ["receive" in FreeRTOS terms] - not both.
正确使用的一个信号是从一个任务向另一个发信号。从每个使用被保护共享资源的任务来看,总是认为,一个互斥意味着获得和释放。相比之下,使用信号灯的任务不是发信号[在FreeRTOS里“发送”]就是在等信号[在FreeRTOS里“接收”]——不能同时。
互斥被用来保护共享资源。一个使用共享资源的任务获得互斥,接着释放互斥。当有另一个任务占据互斥时,没有一个任务可以获得这个互斥。这就是保证,在同一时刻仅有一个任务可以使用共享资源。
一个任务向另一个任务发信号时使用信号灯。以下引用Barr的文章:
For example, Task 1 may contain code to post (i.e., signal or increment) a particular semaphore when the "power" button is pressed and Task 2, which wakes the display, pends on that same semaphore. In this scenario, one task is the producer of the event signal; the other the consumer.
举例来说,任务一可能包含当“电源”按钮被按下时,发布(即,发信号或增加信号量)一个特定的信号灯的代码,并且唤醒显示屏的任务二,取决于同一个信号灯。在这种情况下,一个任务是发信号事件的制造者;另一个是消费者。
如果你是在信号灯和互斥有任何疑问,请查阅Michael的文章。
电子发烧友App









评论