FreeRTOS 任务调度 系统节拍

@(嵌入式)

Freertos

FreeRtos

简述

文章 < FreeRTOS 任务调度 任务切换 > 记录了 FreeRTOS 中任务切换的过程, 提到触发任务切换的两种情况 : 高优先级任务就绪抢占和同优先级任务时间共享(包括提前挂起)。 系统中,时间延时和任务阻塞,时间片都以 Systick 为单位。

通过设置文件 FreeRTOSConfig.hconfigTICK_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;

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

当任务由于等待事件(延时,消息队列什么的堵塞)时,会设置一个时间,这时候,响应的任务会被挂到延时链表中,如果超过设置时间没有事件响应,则系统会从延时链表中取出任务恢复就绪。
系统任务延时参考系统节拍计数器 xTickCount, 加入链表前依据当前计数器的值计算出超时的值 ( xTickCount+ xTicksToDelay ), 顺序插入到延时链表中。

对不同平台xTickCount 表示的位数不同,但是每次节拍中断加一,总会溢出。 上述计算任务延时时间,如果系统发现计算出来的时间已经溢出,则会将该任务加入到 pxOverflowDelayedTaskList 这个链表中。
在系统节拍中断时, 节拍计数器每次加一, 系统判断是否溢出,如果溢出, 调用宏 taskSWITCH_DELAYED_LISTS()切换上述的链表指针。
宏主要实现如下 :

pxTemp = pxDelayedTaskList;
pxDelayedTaskList = pxOverflowDelayedTaskList;
pxOverflowDelayedTaskList = pxTemp;
xNumOfOverflows++;
prvResetNextTaskUnblockTime();

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

唤醒超时任务

全局变量 xNextTaskUnblockTime 记录下一个需要退出延时链表的任务时间, 因此, 接下来判断当前时间,延时链表中是否有任务需要推出阻塞状态。

if( xConstTickCount >= xNextTaskUnblockTime )
{
    for ( ;; ) {
    // 取出唤醒任务, 推进就绪链表
    }
}

对应的, 把所有阻塞时间达到的任务取出, 推入到就绪链表,更新下一个任务解除时间给变量 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;
}

系统延时函数

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

普通延时函数 vTaskDelay

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

void vTaskDelay( const TickType_t xTicksToDelay );

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

**循环延时函数 vTaskDelayUntil **

相比上面的普通延时函数, 这个函数适用于任务周期性执行的。
举个例子说明下, 有一个任务, 需要周期性 500ms 读取一次传感器数据, 用上例子可以这么写 :

void vTASKReadSensor(void *pvParameters)
{
    // 500ms 转换为 节拍
    const portTickType xDelay = pdMS_TO_TICKS(500);
    for( ;; )
    {    
            readSensor();
            vTaskDelay( xDelay );
    }    
}

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

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

多了一个参数 pxPreviousWakeTime, 就不会有这个问题了
先看以下如何使用 :

void vTASKReadSensor(void *pvParameters)
{
    const portTickType xDelay = pdMS_TO_TICKS(500);
    static portTickType xLastWakeTime;
    // 记录第一次调用函数的时间 , 后续该变量由延时函数自己叠加
    xLastWakeTime = xTaskGetTickCount();
    
    for( ;; )
    {    
            readSensor();
            vTaskDelayUntil( xDelay );
    }    
}

周前性执行前调用一个变量, 获取当前节拍计数器 ,简单认为是第一次调用的时间, 而后开始周期性执行, 传入的变量第一次由我们设置后, 后续会由函数自动更新。
比如, 我们在SystickCount 为 0 开始延时, 在500 返回读取数据, 再延时, 和上一个例子一样, 当 500 延时后返回, 调度原因延迟, 等到 600 才读取数据并开始下一次延时, 这里, 这个函数不同地方在于, 他会考虑这延迟的 100, 而第二次延时的时间, 其实还是从 500 开始算的, 也就是, 1000 的时候, 任务延时第二次就结束了, 而不是等到 1100 。

由于涉及到任务调度, 所以, 理论上来说, 两个函数定时都是"不住确"的。 时间单位是系统节拍 !

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • @(嵌入式) 简述 前面文章 < FreeRTOS 任务调度 任务创建 > 介绍了 FreeRTOS 中如何创建任...
    orientlu阅读 8,933评论 1 3
  • 1 嵌入式操作系统 为什么要用嵌入式操作系统 普通的单片机编程:程序(软件)——单片机硬件; 嵌入式操作系统开发:...
    安安zoe阅读 771评论 0 1
  • 每一个喜剧演员都是伟大的,因为他们让这个世界有了更多的欢乐。 每一个喜剧演员也是悲哀的,因为我们无法了解他们背后的...
    洛漾熙_0d2a阅读 1,555评论 0 12
  • #毕业一周年#专题,第二期。 高中开始,我就养成了一个习惯,就是每到夏天来临的时候,总是要用力的回忆起过去一年发生...
    去做一件事阅读 238评论 0 0
  • 好想有这样的一个小院,生活在这方寸天地之间,一砖一瓦都这么美丽有韵味,春天在院子里种种花草,栽个小树;夏天躺在凉椅...
    简之如素阅读 186评论 0 0