Linvis Blog

FreeRTOS-schedule

2019-07-13

任务

FreeRTOS的任务处理函数结构比较简单,函数内不允许return,必须运行在一个无限循环中。
如果不需要,最好显示的删除该任务。

任务状态

类似常见的操作系统任务状态,FreeRTOS有几个简单的任务状态。

大部分任务,都运行在Running和Blocked这两种状态之间。

调度策略

这里,重点看一下任务的调度策略
首先,FreeRTOS支持抢占式(configUSE_PREEMPTION)和时间片轮转(configUSE_TIME_SLICING),并且两个都是默认开启的。
由此,通关配置这两个宏,可以实现不同的调度策略。

策略1

1
2
configUSE_PREEMPTION = 1
configUSE_TIME_SLICING = 1

默认策略,时间片轮转,允许抢占。

task3就抢占了task2。
这里可以注意到,IDLE task也被默认分片了,可以通过configIDLE_SHOULD_YIELD=1关闭IDLE task的分片。

策略2

1
2
configUSE_PREEMPTION = 1
configUSE_TIME_SLICING = 0

仅允许抢占,task会持续运行,直至进入block状态(或者被强占),其他task才能够运行。

策略3

1
2
configUSE_PREEMPTION = 0
configUSE_TIME_SLICING = 1

关闭抢占,但这不符合FreeRTOS的初衷,然后我试了关闭抢占,task只调度了一次,然后就stuck了,
taskYIELD()也不管用,很奇怪。

代码实现

FreeRTOS基于tick机制,
tick由中断产生,中断入口是xPortSysTickHandler,在arm上指向SysTick_Handler的中断向量,有硬件时钟产生。

xPortSysTickHandler主要调用xTaskIncrementTick
然后调用portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
executes all interrupts must be unmasked. There is therefore no need to
save and then restore the interrupt mask value as its value is already
known. */
portDISABLE_INTERRUPTS();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}

xTaskIncrementTick里的大致实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;

/* Called by the portable layer each time a tick interrupt occurs.
Increments the tick then checks to see if the new tick value will cause any
tasks to be unblocked. */
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/* Minor optimisation. The tick count cannot change in this
block. */
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;

/* Increment the RTOS tick, switching the delayed and overflowed
delayed lists if it wraps to 0. */
xTickCount = xConstTickCount;

if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* See if this tick has made a timeout expire. Tasks are stored in
the queue in the order of their wake time - meaning once one task
has been found whose block time has not expired there is no need to
look any further down the list. */
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* The delayed list is empty. Set xNextTaskUnblockTime
to the maximum possible value so it is extremely
unlikely that the
if( xTickCount >= xNextTaskUnblockTime ) test will pass
next time through. */
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* The delayed list is not empty, get the value of the
item at the head of the delayed list. This is the time
at which the task at the head of the delayed list must
be removed from the Blocked state. */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

if( xConstTickCount < xItemValue )
{
/* It is not time to unblock this item yet, but the
item value is the time at which the task at the head
of the blocked list must be removed from the Blocked
state - so record the item value in
xNextTaskUnblockTime. */
xNextTaskUnblockTime = xItemValue;
break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* It is time to remove the item from the Blocked state. */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );

/* Is the task waiting on an event also? If so remove
it from the event list. */
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}

/* Place the unblocked task into the appropriate ready
list. */
prvAddTaskToReadyList( pxTCB );

/* A task being unblocked cannot cause an immediate
context switch if preemption is turned off. */
#if ( configUSE_PREEMPTION == 1 )
{
/* Preemption is on, but a context switch should
only be performed if the unblocked task has a
priority that is equal to or higher than the
currently executing task. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}

/* Tasks of equal priority to the currently running task will share
processing time (time slice) if preemption is on, and the application
writer has not explicitly turned time slicing off. */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

#if ( configUSE_TICK_HOOK == 1 )
{
/* Guard against the tick hook being called when the pended tick
count is being unwound (when the scheduler is being unlocked). */
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
}
else
{
++uxPendedTicks;

/* The tick hook gets called at regular intervals, even if the
scheduler is locked. */
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}

#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */

return xSwitchRequired;
}

分析如下:

  1. 增加tick计数器xTickCount
  2. 根据pxDelayedTaskList中,第一个task的到期时间,判断xTickCount,是否到达
  3. 如果到达了
    a. 将第一个task从delayList删除
    b. 删除它的xEventListItem(xEventListItem是用于监听信号量等这些事件的)
    c. 然后把它加到readyList(prvAddTaskToReadyList( pxTCB ))

这里有两种允许任务发生切换的条件

  1. 如果这个task优先级比当前执行的task优先级更高,xSwitchRequired = pdTRUE,允许发生任务切换
  2. 如果readyList里的任务优先级大于1,并且时间片轮转开启的话,则允许任务切换xYieldPending != pdFALSE的情况下,这里暂时没明白它的用处,推测是上层应用需要发生一次上下文切换

然后我们看portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
这一句的目的是什么呢?
查阅资料,得知

这一句,根据注释,得知,是配置了这个寄存器,然后发生了上下文切换,这里,我对照的是Cortex-M4,发现是修改了这个地址0xe000ed04上的第28bit,查阅其手册,得知,该bit表示PendSV set-pending bit.
这里提到,SystemTick的中断是优先级很高的,但是上下文切换不一定有很高的优先级,所以,如何让高优先级服务低优先级的耗时操作?PendSV为此产生,PendSV表示可延时处理的操作,类似Linux中的tasklet,PendSV被置位后,CPU会等待处理完其他优先级高、紧急的中断事件,处理完后,产生PendSV中断,处理耗时的操作。

所以,最终会执行这个中断处理:
xPortPendSVHandler,这里面是汇编的实现

这个中断,会保存当前task的上下文寄存器,然后调用vTaskSwitchContext完成上下文切换

那么,vTaskSwitchContext里干了什么呢,翻阅代码,可以得知,函数内部,主要是调用taskSELECT_HIGHEST_PRIORITY_TASK(),
从pxReadyTasksLists,挑选优先级最高的taskList,然后赋值给pxCurrentTCB,函数执行完,返回xPortPendSVHandler,
继续执行汇编,恢复pxCurrentTCB里的第一个item的寄存器等上下文,完成task的切换。

以上,是FreeRTOS任务调度的一个简单分析。

扫描二维码,分享此文章