摘要

任务睡眠函数是一个非常有用的操作系统API,几乎每个RTOS都提供了一个类似的API给应用程序调用,在ucosii里,它叫OSTimeDly;在Nucleus里,它叫NU_Sleep;在FreeRTOS里,它叫vTaskDelay。它们的目的都是一样的,告诉操作系统:“我现在没有事情要做,请把CPU分配给其它任务,并在某个时间点把我唤醒”,这个时间点就是函数的入参,一般都是以tick为单位,如下所示:

关于tick和时间片的详细说明见实时操作系统的任务调度示例之时间片。睡眠函数的使用方法非常简单,本文跳过了它的基本介绍,在STM32平台上做了几个平时不常见的睡眠函数实验,结合实验结果讲解了FreeRTOS里的vTaskDelay的实现;后面对比了几种RTOS对于sleep的实现细节的不同之处。

在中断处理里调用任务睡眠函数

相信很多朋友都在各种资料都看到过这样的说明:中断处理函数里不能调用任务睡眠函数。楼主在也先声明一下,这句话是正确的,确实不能,这里要做这个实验只是为了加深对任务睡眠函数实现的理解。但是如果在中断处理函数里调了,会有什么后果?系统崩溃?或者中断堆栈和任务堆栈互相覆盖?还是跟没发生什么事一样? 答案是“不一定”。请看下面的这个实验:

void Task1Func(void* p)
{static int cnt = 0;   while (1){USART_OUT(USART1,"Task 1 is running %d\n",++cnt);if (3 == cnt)  //到第3次循环的时候,启动一个定时器中断{TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);TIM_Cmd(TIM3, ENABLE);USART_OUT(USART1,"IRQ finished Task1 go on %d\n",++cnt);}vTaskDelay(200);  //睡眠2秒}
}
void Task2Func(void* p){static int cnt = 0;vTaskDelay(100);while (1){      USART_OUT(USART1,"Task 2 is running %d\n",++cnt);vTaskDelay(200);       //睡眠2秒}
}
xTaskCreate(Task1Func,( const signed char * )"Task1",64,NULL,tskIDLE_PRIORITY+3,NULL);
xTaskCreate(Task2Func,( const signed char * )"Task2",64,NULL,tskIDLE_PRIORITY+3,NULL);
void TIM3_IRQHandler(void)
{static int cnt = 0;if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){cnt++;TIM_ClearITPendingBit(TIM3, TIM_IT_Update);USART_OUT(USART1,"IRQ\n");  //输出一句log表明走到了这里vTaskDelay(600);            //调用睡眠函数,6秒TIM_Cmd(TIM3, DISABLE);     //关闭TIM3中断TIM_ITConfig(TIM3,TIM_IT_Update,DISABLE); }
}

在main函数里创建了2个Task,都是做着简单的打印、输出log的无限循环。当Task1到第3次循环时,触发一个定时器中断,并在中断里调用任务睡眠函数vTaskDelay(600)。定时器中断的配置在 实时操作系统的忙等延迟实现里有讲述
程序运行起来输出的串口log如下:

扫一眼log,就能看出,系统并没有崩溃,而是在正常运行。请重点关注红色框内部的log,10:50:06.531 Task1进入第3次循环,输出一句打印“Task 1 is running 3”之后,触发TIM3的中断,该中断会立即抢占任务执行,输出打印“IRQ”,然后执行vTaskDelay(600),神奇的一幕出现了,这句代码的效果发生在Task1身上!Task睡眠了6秒,才完成睡眠走到IRQ finished Task1 go on。

为什么在中断里睡眠结果却是误把任务“给睡了”?答案当然要去vTaskDelay里面去找。直接看代码吧

void vTaskDelay( portTickType xTicksToDelay )
{portTickType xTimeToWake;signed portBASE_TYPE xAlreadyYielded = pdFALSE;if( xTicksToDelay > ( portTickType ) 0U ) //如果入参设为0,就等于执行一次调度器{vTaskSuspendAll();  //关闭调度器{xTimeToWake = xTickCount + xTicksToDelay;  //计算睡眠醒来的时间vListRemove( ( xListItem * ) &( pxCurrentTCB->xGenericListItem ) );  //将当前任务从就绪列表里prvAddCurrentTaskToDelayedList( xTimeToWake );                  //将当前任务加入到阻塞任务链表里}xAlreadyYielded = xTaskResumeAll();  //打开调度器,返回值的意义是“是否已经完成了一次重新调度”}if( xAlreadyYielded == pdFALSE )  //如果没有完成,再强制执行一次任务重新调度{portYIELD_WITHIN_API(); }
}

在这个实验里,任务1正在执行的过程中,被中断抢占,所以pxCurrentTCB指向的还是任务1的任务控制块,所以等于对该任务执行了睡眠操作。

将代码稍微修改一下,TIM3的中断周期设为500ms,第一次直接返回(因为刚打开的时候就会触发一次中断),第二次和前面的操作相同

void TIM3_IRQHandler(void)
{static int cnt = 0;if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){TIM_ClearITPendingBit(TIM3, TIM_IT_Update);if (1 == ++cnt) //第一次直接返回,第二次执行下面的delay函数return;vTaskDelay(600);TIM_Cmd(TIM3, DISABLE);TIM_ITConfig(TIM3,TIM_IT_Update,DISABLE); }}

得到的运行结果就完全不同了,

Task1执行了第3次循环之后500ms,TIM3的中断输出了IRQ打印,之后系统就死了。怎么死的?

此时Task1和Task2都处在睡眠状态,pxCurrentTCB指向的是空闲任务idletask,idletask是什么?这个任务是的系统里的一个酱油任务,它的优先级设置为最低,当所有应用程序的任务都不在就绪状态时,就轮到它执行了。它虽然不执行具体的工作,但它很重要,可以说是整个RTOS的最后一道防线。还是用考虑死机时的运行情况,在程序输出IRQ的时候,系统正在中断里,vTaskDelay将idletask从就绪链表中摘除后,整个系统里就没有一个任务还处在就绪态了。vTaskDelay的结尾会触发PendSV中断,它是低级别的中断,在当前中断执行完毕后,跳到它的入口xPortSendSVHandler,它其实就是执行调度算法,查找当前系统里最高优先级的就绪态任务,并执行它,查找最高优先级就绪任务代码如下:

 while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) ){--uxTopReadyPriority;  //优先级从大到小循环遍历就绪任务的数组pxReadyTaskList}

平时即使所有的应用task都睡眠了,也没有关系,因为调度器会反复的执行idletask打打酱油。但是这次不同,连idletask都不在就绪态了,idletask的优先级为0,uxTopReadyPriority一直减小到0都找不到可执行的task,uxTopReadyPriority被翻转了,0-1=0xFFFF!再去访问pxReadyTasksLists的0xFFFF项元素,内存严重越界,死机就是唯一的结果。

睡眠函数的时间单位都是tick吗?

一般我们都说睡眠函数都是以tick为单位的,从前面的实验输出的log看来,Task1和Task2任务轮流睡眠1秒,等于100个tick,从时间戳上看还是相当精确的。是不是一直都是这么精确呢?请看下面的实验:

void do_something()
{m_delay(60);
}
void Task1Func(void* p)
{static int cnt = 0;while (1){USART_OUT(USART1,"Task 1 is running %d\n",++cnt);do_something();USART_OUT(USART1,"Task 1 work done\n");vTaskDelay(1);}
}

该实验只创建了一个任务,它调了一个函数do_something之后睡眠1个tick之后继续循环(为了实验结果更好,将tick间隔设的较大,1个tick是100ms),do_something模拟一项计算量较大的任务,实际上就是原地打转了60ms。按照代码的布置,作者的意图应该是像下面这张图这样的:

实际运行情况是这样吗?看看输出的log:

任务先工作了大约60ms,然后只能睡眠约40ms又开始下次循环,而不是前面分析的期望能睡眠100ms。

从这个实验我们知道任务睡眠1个tick的真正含义是“睡眠,直到下次tick中断到来”,用时间图来表示是这样的

睡眠函数与唤醒的几种实现方式对比

FreeRTOS的实现

前面已经贴过了vTaskDelay的代码,它先将当前任务从就绪链表中摘下来,挂接到delay链表中,

传入的参数xTimeToWake是预期的唤醒时间,vListInsert插入链表的动作会先以唤醒时间的tick count做一个排序,根据唤醒时间由近及远的方式存储每个链表节点,xNextTaskUnBlockTime是链表头节点的唤醒时间,也就是距离现在最近的要唤醒的任务时间。

大多数RTOS的睡眠唤醒、定时器超时等检查都是在tick的中断做的,包括本文所讲的3种系统。FreeRTOS的检查任务唤醒函数如下

void  prvCheckDelayedTasks()
{                                                       portTickType xItemValue;                                            if( xTickCount >= xNextTaskUnblockTime ) //如果当前时间已经大于下一次要唤醒任务的时间{           for( ;; )   //用了一个循环,因为可能会有多个任务在同一时间要唤醒{                                                         if( listLIST_IS_EMPTY( pxDelayedTaskList ) == FALSE ) {           pxTCB = ( tskTCB * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xGenericListItem ) );                                                                       if(xTickCount < xItemValue){   //这是下次要超时的任务时间                        xNextTaskUnblockTime = xItemValue;                             break;}vListRemove( &( pxTCB->xGenericListItem ) ); //将要唤醒的任务从delay链表摘下prvAddTaskToReadyQueue( pxTCB );             //挂接到就绪链表中}}}
}

Nucleus的实现

Nucleus在任务控制块TCB里存有一个timer计时的结构体,它本身是一个双向链表的节点。Nucleus里将任务超时和定时器超时只用了同一条链表来进行管理,只有一个标志位作为区分。任务睡眠时,类似于定时器的启动,将timer结构体按超时时间挂到等待链表上,系统的头节点里保存是“距离当前最近的一个超时定时器或任务的时间”,后面的节点按照时间的由近及远,每个节点里的时间都是相对于前一个的值。详细的描述见这里 高效软件定时器的设计

总结来看,FreeRTOS和Necleus里关于的实现都是比较高效的,二者思路差不多,一个用绝对时间,一个用相对时间,旗鼓相当

最后是ucosii的实现:

在任务睡眠的时候调用的是OSTimeDly,这里有个细节需要点赞,ucosii2.86的代码里,对是否在中断里调用该函数进行了判断(下面图片里的红框),如果在中断上下文就直接返回,避免了前面实验里的错误。  2.52里还没有这个功能。这个函数主要就是做了两件事:

1 在任务就绪表里将该任务的就绪态清除

2 记录睡眠的时间(图片里的黑色框)

关于唤醒的检查,ucosii的实现在每个tick中断里遍历所有任务并检查每个任务的OSTCBDly字段,非常简单粗暴。如果系统里没有任务在睡眠的话,那每个tick周期的循环遍历就没有做任何有意义的事,相对于前面两个RTOS,ucosii的做法有可优化的空间

实时操作系统的任务睡眠相关推荐

  1. Vxworks、QNX、Xenomai、Intime、Sylixos、Ucos等实时操作系统的性能特点

    Vxworks.QNX.Xenomai.Intime.Sylixos.Ucos等实时操作系统的性能特点 VxWorks操作系统 VxWorks 操作系统是美国WindRiver公司于1983年设计开发 ...

  2. px4原生源码学习三--Nuttx实时操作系统的使用

    /*************************************************************************************************** ...

  3. ITRON入门学习之实时操作系统的意义与价值

    实时操作系统 什么是实时操作系统? 通用操作系统的类型 实时操作系统是什么? TRON项目与ITRON的关系 ITRON规格是什么? 什么人应该学习实时操作系统 比起Windows和Linux这样的操 ...

  4. ITRON入门之实时操作系统的特点

    实时操作系统 概述 实时操作系统的系统定位 安装实时操作系统的优点 可以扩大开发规模 操作系统为我们提供各种功能 可以同时完成很多任务 安装实时操作系统的缺点 使用CPU/内存资源 需要处理实时操作系 ...

  5. 图解实时操作系统和非实时操作系统的区别

    对于实时操作系统(RTOS)和非实时操作系统,你能分别列举出来多少? 实时操作系统:uCOS/VxWorks/RTLinux 非实时操作系统:Linux/Windows/OSX 我也只能列举出来这么多 ...

  6. 实时操作系统和非实时操作系统的区别

    实时操作系统:uCOS/VxWorks/RTLinux 非实时操作系统:Linux/Windows/OSX 他们之间的区别,详见下图: 在上面的图中右边的任务优先级高于左边的任务,先看实时操作系统的, ...

  7. 嵌入式实时操作系统的基本概念

    第一章 嵌入式实时操作系统的基本概念 1.1计算机操作系统 1.计算机是一种功能强大的数字运算装置,由中央微处理器(CPU),存储器,接口及外部设备等物理装置构成,构成计算机的物理装置即硬件系统. 2 ...

  8. 【计算机架构】什么是实时操作系统、什么是非实时操作系统;实时操作系统和非实时操作系统的区别

    一.实时操作系统 实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成 ...

  9. 《基于嵌入式实时操作系统的编程技术》笔记清单:第六章行为同步

    <基于嵌入式实时操作系统的编程技术>笔记清单:第三章任务划分. <基于嵌入式实时操作系统的编程技术>笔记清单:第四章任务设计. <基于嵌入式实时操作系统的编程技术> ...

最新文章

  1. tensorflow打印模型结构_Tensorflow上手1: Print与py_func
  2. mongodb中分页显示数据集的学习
  3. java并行计算同步返回_Java大文本并行计算实现过程解析
  4. C++ Vector 使用心得
  5. flow hive 新型蜂箱_全新Flow Hive 2使得养蜂人获取蜂蜜更轻松
  6. C语言经典递归算法之和式分解
  7. NFS还是iSCSI?关于VMware vSphere的存储连接的选择题
  8. DRLSE 水平集算法总结
  9. live2d_Live2d( 动画制作软件 )中文版分享
  10. Fiddler中文版汉化插件 0.1
  11. 随机信号分析学习笔记(6)
  12. 手游入门必备基础知识
  13. 人行征信报告介绍(一)
  14. cherry键盘使用备忘
  15. 机工士姆斯塔迪奥分数 20作者 DAI, Longao单位 杭州百腾教育科技有限公司
  16. 如何在WIN10系统中设置护眼颜色绿豆沙?
  17. html网页抓取建一个网站前端,创建网页的方法以及生成HTML骨架
  18. 用BibTeX 写 Reference
  19. python 统计文本字数 生成词云图
  20. java程序员自我简介及简历

热门文章

  1. SpringBoot(23) 集成socket.io服务端和客户端实现通信
  2. ZigBee产品认证指南
  3. java模板/策略模式打怪兽
  4. 教育局网站群建设思路
  5. 什么是JWT?(细致讲解)
  6. 全面解决Generic host process for win32 services遇到问题需要关闭
  7. CAD导出插件逆向分析
  8. iText7使用IExternalSignatureContainer进行签名和验签
  9. 软件的23种设计模式的通俗解释
  10. 3970x做网站服务器,锐龙ThreadRipper 3970X为原生32核 开核变64核别想了