您的位置:首页 > 其它

嵌入式操作系统之时钟节拍下的任务切换

2016-12-07 22:44 302 查看
    

FreeRTOS 中任务切换的过程, 提到触发任务切换的两种情况 : 高优先级任务就绪抢占和同优先级任务时间共享(包括提前挂起)。 系统中,时间延时和任务阻塞,时间片都以 Systick 为单位。通过设置文件 
FreeRTOSConfig.h
 中 
configTICK_RATE_HZ
 设置任务节拍中断频率,
在启动任务调度器时,系统会根据另一个变量, CPU 的频率
configCPU_CLOCK_HZ
 计算对应写入节拍计数器的值,启动定时器中断。

系统在每一次节拍计数器中断服务程序
xPortSysTickHandler
(平台实现 port.c 中) 中调用处理函数 xTaskIncrementTick,
依据该函数返回值判断是否需要触发 PendSV 异常, 进行任务切换。 

涉及任务时间片轮循, 任务阻塞超时, 以及结束以此实现的延时函数。

分析的源码版本是 v9.0.0


xTaskIncrementTick()

系统每次节拍中断服务程序中主要任务由函数 
xTaskIncrementTick
 完成。 

在任务调度器没有挂起的情况下( xTaskIncrementTick != pdFALSE ),该函数主要完成 : 

* 判断节拍计数器xTickCount 是否溢出, 溢出轮换延时函数队列 

* 判断是否有阻塞任务超时,取出插入就绪链表 

* 同优先级任务时间片轮

而当任务调度器被挂起时, 该函数累加挂起时间计数器 
uxPendedTicks
, 调用用户钩子函数, 此时,正在运行的任务不会被切换, 一直运行。 

当恢复调度时, 系统会先重复调用 
xTaskIncrementTick
 补偿 (
uxPendedTicks
次)。

不管, 系统调度器是否挂起, 每次节拍中断都会调用用户的钩子函数 
vApplicationTickHook
。 由于函数是中断中调用,不要在里面处理太复杂的事情!!

节拍计数器溢出

涉及的变量, 定义在 
task.c
开头。
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
1
2
3
4
1
2
3
4

初始化时, 
pxDelayedTaskList
 指向 
xDelayedTaskList1
, 
pxOverflowDelayedTaskList
 指向 
pxOverflowDelayedTaskList
,一开始我还在郁闷延时链表为什么要两个,到这里才明白。

当任务由于等待事件(延时,消息队列什么的堵塞)时,会设置一个时间,这时候,响应的任务会被挂到延时链表中,如果超过设置时间没有事件响应,则系统会从延时链表中取出任务恢复就绪。 

系统任务延时参考系统节拍计数器 
xTickCount
, 加入链表前依据当前计数器的值计算出超时的值 ( xTickCount+ xTicksToDelay ), 顺序插入到延时链表中。

对不同平台
xTickCount
 表示的位数不同,但是每次节拍中断加一,总会溢出。 上述计算任务延时时间,如果系统发现计算出来的时间已经溢出,则会将该任务加入到 
pxOverflowDelayedTaskList
 这个链表中。 

在系统节拍中断时, 节拍计数器每次加一, 系统判断是否溢出,如果溢出, 调用宏 
taskSWITCH_DELAYED_LISTS()
切换上述的链表指针。 

宏主要实现如下 :
pxTemp = pxDelayedTaskList;
pxDelayedTaskList = pxOverflowDelayedTaskList;
pxOverflowDelayedTaskList = pxTemp;
xNumOfOverflows++;
prvResetNextTaskUnblockTime();
1
2
3
4
5
1
2
3
4
5

这就是设置两个链表的原因,轮流倒应对计数器的溢出。

唤醒超时任务

全局变量 
xNextTaskUnblockTime
 记录下一个需要退出延时链表的任务时间, 因此, 接下来判断当前时间,延时链表中是否有任务需要推出阻塞状态。
if( xConstTickCount >= xNextTaskUnblockTime )
{
for ( ;; ) {
// 取出唤醒任务, 推进就绪链表
}
}
1
2
3
4
5
6
1
2
3
4
5
6

对应的, 把所有阻塞时间达到的任务取出, 推入到就绪链表,更新下一个任务解除时间给变量 
xNextTaskUnblockTime

任务时间片轮循

处理完延时任务后, 开始判断当前运行任务, 对应优先级链表中是否有其他任务就绪, 如果有,需要保证每个任务都能获得运行时间, 标记需要任务切换, 作为函数返回。

完整函数

完整函数注释如下,
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;

traceTASK_INCREMENT_TICK( xTickCount );
// 调度器正在运行
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
// 节拍计数器递增 1
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;

if( xConstTickCount == ( TickType_t ) 0U )
{
// 节拍计数器溢出
// 比如32位 0xFFFFFFFF + 1 -> 0
// 延时链表切换
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}

if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
// 没有任务延时, 时间设置"无穷大" 退出循环
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
// 取出延时链表头任务 TCB
pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
// 取该任务延时值
xItemValue = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem));
// 判断任务是否超时
if( xConstTickCount < xItemValue )
{
// 任务还没到时间,更新全局变量
// 直接退出
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
// 任务恢复就绪, 从堵塞的链表中删除
( void ) uxListRemove( &( pxTCB->xStateListItem ) );

if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
// 插入就绪链表等待被执行
prvAddTaskToReadyList( pxTCB );
// 如果系统允许抢占
#if (  configUSE_PREEMPTION == 1 )
{
// 如果新就绪任务优先级高于当前任务
// 标记需要切换任务
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
}
}

// 同优先级任务 时间轮
#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 )
{
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICK_HOOK */
}
else
{
// 记录调度器被挂起器件节拍中断次数
// 恢复后用于补偿, 执行本函数 uxPendedTicks 次先
++uxPendedTicks;
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}

// 函数返回值, 如果为 pdTRUE,
// 则调用的系统节拍中断会触发 PendSV 异常, 任务切换
#if ( configUSE_PREEMPTION == 1 ) // 允许抢占
{
// 其他地方标记需要执行一次任务切换
// 所以不管前面需不需要 这里都会返回需要切换
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */

return xSwitchRequired;
}
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
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

系统延时函数

任务执行过程中需要使用到延时函数进行延时, 使用系统提供的延时函数可以将当前任务挂起,让出CPU 使用时间,当时间到达的时候, 有系统恢复任务运行。 FreeRTOS 提供两种类型的延时函数


普通延时函数 vTaskDelay

一般情况下,需要延时一定时间,就调用此函数,将需要的延时时间转换为对应系统节拍数传递(如宏pdMS_TO_TICKS()), 之后,当前任务会从就绪链表移除, 加入到延时链表中,系统会在节拍中断中检查是否到达延时时间, 重新恢复任务就绪。
void vTaskDelay( const TickType_t xTicksToDelay );
1
1

该函数调用到另一个函数是 
prvAddCurrentTaskToDelayedList
, 将任务加入到延时链表中, 函数中会判断设定时间是否溢出, 选择加入到对应的延时链表, 同上提到计数器溢出的问题。


循环延时函数 vTaskDelayUntil

相比上面的普通延时函数, 这个函数适用于任务周期性执行的。 

举个例子说明下, 有一个任务, 需要周期性 500ms 读取一次传感器数据, 用上例子可以这么写 :
void vTASKReadSensor(void *pvParameters)
{
// 500ms 转换为 节拍
const portTickType xDelay = pdMS_TO_TICKS(500);
for( ;; )
{
readSensor();
vTaskDelay( xDelay );
}
}
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

看起来是周期性 500 ms 执行, 但是考虑, 如果任务由于优先级比较低之类的问题, 在延时返回就绪状态后没有及时被运行,那么实际时间就开始飘了。 

如果使用函数 
vTaskDelayUntil

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
1
1

多了一个参数 
pxPreviousWakeTime
, 就不会有这个问题了 

先看以下如何使用 :
void vTASKReadSensor(void *pvParameters)
{
const portTickType xDelay = pdMS_TO_TICKS(500);
static portTickType xLastWakeTime;
// 记录第一次调用函数的时间 , 后续该变量由延时函数自己叠加
xLastWakeTime = xTaskGetTickCount();

for( ;; )
{
readSensor();
vTaskDelayUntil( xDelay );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
1
2
3
4
5
6
7
8
9
10
11
12
13

周前性执行前调用一个变量, 获取当前节拍计数器 ,简单认为是第一次调用的时间, 而后开始周期性执行, 传入的变量第一次由我们设置后, 后续会由函数自动更新。 

比如, 我们在SystickCount 为 0 开始延时, 在500 返回读取数据, 再延时, 和上一个例子一样, 当 500 延时后返回, 调度原因延迟, 等到 600 才读取数据并开始下一次延时, 这里, 这个函数不同地方在于, 他会考虑这延迟的 100, 而第二次延时的时间, 其实还是从 500 开始算的, 也就是, 1000 的时候, 任务延时第二次就结束了, 而不是等到 1100 。

由于涉及到任务调度, 所以, 理论上来说, 两个函数定时都是”不住确”的。 时间单位是系统节拍 !
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐