实时操作系统的任务睡眠

摘要


任务睡眠函数是一个非常有用的操作系统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的做法有可优化的空间





  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
我可以为您提供一个参考解答: ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define NUM_CUSTOMERS 10 int waiting_room[NUM_CUSTOMERS]; int num_waiting = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t barber_cv = PTHREAD_COND_INITIALIZER; pthread_cond_t customer_cv = PTHREAD_COND_INITIALIZER; void *barber(void *arg) { while (1) { pthread_mutex_lock(&mutex); // 如果没有顾客等待,则理发师睡觉 while (num_waiting == 0) { printf("Barber is sleeping...\n"); pthread_cond_wait(&customer_cv, &mutex); } // 取出最前面的等待顾客 int customer = waiting_room[0]; // 将其他等待顾客往前移动一位 for (int i = 1; i < num_waiting; i++) { waiting_room[i - 1] = waiting_room[i]; } num_waiting--; pthread_mutex_unlock(&mutex); // 为等待的顾客理发 printf("Barber is cutting hair for customer %d\n", customer); sleep(3); // 理发师理发的时间 printf("Barber has finished cutting hair for customer %d\n", customer); } pthread_exit(NULL); } void *customer(void *arg) { int id = *(int *)arg; pthread_mutex_lock(&mutex); // 如果等候室已满,则离开 if (num_waiting == NUM_CUSTOMERS) { printf("Customer %d leaves because waiting room is full\n", id); pthread_mutex_unlock(&mutex); pthread_exit(NULL); } // 进入等候室 waiting_room[num_waiting] = id; num_waiting++; // 叫醒睡觉的理发师 pthread_cond_signal(&customer_cv); // 等待理发师完成理发 while (id != waiting_room[0]) { printf("Customer %d is waiting...\n", id); pthread_cond_wait(&barber_cv, &mutex); } // 离开理发店 for (int i = 1; i < num_waiting; i++) { waiting_room[i - 1] = waiting_room[i]; } num_waiting--; pthread_mutex_unlock(&mutex); // 完成理发 printf("Customer %d has finished cutting hair\n", id); pthread_exit(NULL); } int main() { pthread_t barber_tid; pthread_t customers_tid[NUM_CUSTOMERS]; int ids[NUM_CUSTOMERS]; // 创建理发师线程 pthread_create(&barber_tid, NULL, &barber, NULL); // 创建顾客线程 for (int i = 0; i < NUM_CUSTOMERS; i++) { ids[i] = i + 1; pthread_create(&customers_tid[i], NULL, &customer, &ids[i]); sleep(1); // 创建间隔1秒钟 } // 等待顾客和理发师退出 for (int i = 0; i < NUM_CUSTOMERS; i++) { pthread_join(customers_tid[i], NULL); } pthread_cancel(barber_tid); pthread_join(barber_tid, NULL); return 0; } ``` 这段代码是一个简单的理发师问题的解决方案,利用了 POSIX 线程库的互斥量、条件变量等操作系统进程间通信的机制来协调理发师和顾客任务的执行,并通过睡眠等待的方式实现了进程的挂起和唤醒。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值