FreeRTOS task switching


The core of the RTOS system is task management, and the core of task management is task switching. Task switching determines the execution order of tasks, and the efficiency of task switching also determines the performance of a system, especially for real-time operating systems.

1. PendSV exception

The PendSV (suspendable system call) exception is very important to OS operation, and its priority can be set programmatically. The PendSV interrupt can be triggered by setting bit 28 of the interrupt control and status register ICSR, that is, the pending bit of PendSV, to 1. Unlike SVC exceptions, it is imprecise, so its pending status can be set within higher-priority exception handling, and will be executed after the higher-priority handling is complete.

Using this feature, setting PendSV to the lowest exception priority allows PendSV exception handling to execute after all other interrupt handling is complete, which is useful for context switching and is key in various OS designs.

In a typical system with an embedded OS, processing time is divided into time slices. If there are only two tasks in the system, these two tasks will be executed alternately, as shown in the following figure:

insert image description here
A context switch can be triggered when:
⚫ Executing a system call
⚫ System tick timer (SysTick) interrupt.

In the OS, the task scheduler decides whether context switching should be performed. As shown in the figure above, the task switching is performed by the SysTick interrupt, and each time it decides to switch to a different task.

If the interrupt request (IRQ) is generated before the SysTick exception, the SysTick exception may preempt the IRQ processing. In this case, the OS should not perform context switching, otherwise the interrupt request IRQ processing will be delayed, and in the real system Latency is also often unpredictable -- something that should never be tolerated by any system with even the slightest real-time requirements. For CortexM3 and Cortex-M4 processors, when there is an active exception service, the design does not allow returning to thread mode by default. If there is an active interrupt service and the OS tries to return to thread mode, a usage fault will be triggered, as shown in the figure below .
insert image description here

In some OS designs, to solve this problem, you can run the interrupt service without performing context switching. At this time, you can check the pushed xPSR in the stack frame or the interrupt active status register in the NVIC. However, the performance of the system may be affected, especially when the interrupt source continues to generate requests before and after the SysTick interrupt, so that the context switch may not have the opportunity to execute.

To solve this problem, the PendSV exception delays the context switch request until all other IRQ processing has completed, at which point PendSV needs to be set to the lowest priority. If the OS needs to perform a context switch, it sets the pending state of PendSV and performs the context switch within the PendSV exception. As shown below:

insert image description here
The journal of events in the above figure is recorded as follows:
(1) Task A calls SVC to request a task switch (eg, waits for some work to complete)
(2) OS receives the request, prepares for context switch, and pends a PendSV exception.
(3) When the CPU exits SVC, it immediately enters PendSV, thereby performing a context switch.
(4) After the execution of PendSV is completed, it will return to task B and enter the thread mode at the same time.
(5) An interrupt occurs and the interrupt service routine starts executing
(6) During the execution of the ISR, a SysTick exception occurs and the ISR is preempted.
(7) The OS performs the necessary operations, then pend raises the PendSV exception in preparation for the context switch.
(8) After SysTick exits, return to the previously preempted ISR, and the ISR continues to execute.
(9) After the ISR finishes executing and exits, the PendSV service routine starts to execute, and performs context switching inside.
(10) After the execution of PendSV is completed, return to task A, and the system enters the thread mode again.

To sum up, it can be seen that the task switching of the FreeRTOS system is finally completed in the PendSV interrupt service function, and UCOS also completes the task switching in the PendSV interrupt.

2. FreeRTOS task switching occasions

When explaining the PendSV interrupt in (1), the context (task) switch is triggered:
● A system call can be executed
● System tick timer (SysTick) interrupt.

1. Execute the system call taskYIELD()

Executing the system call is to execute the related API functions provided by the FreeRTOS system, such as the task switching function taskYIELD(). Some API functions of FreeRTOS also call the function taskYIELD(). These API functions will cause task switching. These API functions and the task switching function taskYIELD( ) are collectively referred to as system calls. The function taskYIELD() is actually a macro, which is defined as follows in the file task.h:

#define taskYIELD() portYIELD()

The function portYIELD() is also a macro, defined as follows in the file portmacro.h:

#define portYIELD() 								\
{
      
       													\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ (1)
													\
	__dsb( portSY_FULL_READ_WRITE ); 				\
	__isb( portSY_FULL_READ_WRITE );				\
}

(1) Start the PendSV interrupt by writing 1 to bit28 of the interrupt control and status register ICSR to suspend PendSV. In this way, task switching can be performed in the PendSV interrupt service function.

The interrupt-level task switching function is portYIELD_FROM_ISR(), which is defined as follows:

#define portEND_SWITCHING_ISR( xSwitchRequired ) \
if( xSwitchRequired != pdFALSE ) portYIELD()
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )

It can be seen that portYIELD_FROM_ISR() finally completes task switching by calling the function portYIELD().

2. The system tick timer (SysTick) interrupts SysTick_Handler

Task switching is also performed in the tick timer (SysTick) interrupt service function in FreeRTOS. The tick timer interrupt service function is as follows:

void SysTick_Handler(void)
{
    
     
	 if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
	 {
    
    
		 xPortSysTickHandler();
	 }
}

In the tick timer interrupt service function, the API function xPortSysTickHandler() of FreeRTOS is called. The source code of this function is as follows:

void xPortSysTickHandler( void )
{
    
    
	vPortRaiseBASEPRI(); (1)
	{
    
    
		if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器 xTickCount 的值
		{
    
    
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (2)
		}
	}
	vPortClearBASEPRIFromISR(); (3)
}

(1) Disable the interrupt
(2) Suspend PendSV by writing 1 to bit28 of the interrupt control and status register ICSR to start the PendSV interrupt. In this way, task switching can be performed in the PendSV interrupt service function.
(3) Turn on the interrupt.

3. PendSV interrupt service function PendSV_Handler()

The PendSV interrupt service function should be PendSV_Handler(), but FreeRTOS uses #define to redefine, as follows:

#define xPortPendSVHandler PendSV_Handler

The source code of the function xPortPendSVHandler() is as follows:

__asm void xPortPendSVHandler( void )
{
    
    
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
	
	PRESERVE8
	
	mrs r0, psp 										(1)
	isb
	
	ldr r3, =pxCurrentTCB 								(2)
	ldr r2, [r3]							 			(3)
	
	stmdb r0!, {
    
    r4-r11, r14}							(4)
	str r0, [r2] 										(5)
	
	stmdb sp!, {
    
    r3,r14} 								(6)
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY 		(7)
	msr basepri, r0 									(8)
	
	dsb
	isb
	
	bl vTaskSwitchContext								(9)
	mov r0, #0 											(10)
	msr basepri, r0 									(11)
	ldmia sp!, {
    
    r3,r14} 								(12)
	
	ldr r1, [r3] (13)
	ldr r0, [r1] (14)
	
	ldmia r0!, {
    
    r4-r11} 								(15)
	
	msr psp, r0 										(16)
	
	isb
	
	bx r14												(17)
	nop
}

(1) Read the process stack pointer and save it in register R0.

(2) and (3), obtain the task control block of the current task, and save the address of the task control block in register R2.

(4) Save the values ​​of registers r4~r11 and R14.

(5) Write the value of register R0 into the address saved by register R2, that is, save the new stack top in the first field of the task control block. At this time, the register R0 holds the latest stack top pointer value, so the latest stack top pointer should be written into the first field of the task control block of the current task, and has been obtained after (2) and (3) task control block, and write the first address of the task control block into register R2.

(6) Push the values ​​of registers R3 and R14 to the stack temporarily. The task control block of the current task is saved in register R3, and the function vTaskSwitchContext() is called next. In order to prevent the values ​​of R3 and R14 from being rewritten, here is a temporary Push the values ​​of R3 and R14 onto the stack first.

(7) and (8), turn off the interrupt and enter the critical area

(9) Call the function vTaskSwitchContext(), which is used to obtain the next task to be run, and
update pxCurrentTCB to the task to be run.

(10) and (11), open the interrupt, and exit the critical section.

(12) The values ​​of registers R3 and R14 just saved are popped out of the stack, and the values ​​of registers R3 and R14 are restored. Note that after step (12), the value of pxCurrentTCB has changed at this time, so reading the data at the address saved by R3 will find that its value has changed, and it becomes the task control block of the next task to run.

(13) and (14), obtain the task stack top of the new task to be run, and save the stack top in register R0.

(15), R4~R11, R14 pop out, that is, the scene of the task to be run.

(16). Update the value of the process stack pointer PSP.

(17) After executing this line of code, the hardware automatically restores the values ​​​​of the registers R0~R3, R12, LR, PC and xPSR, and determines whether to enter the processor mode or the process mode after the exception returns, and use the main stack pointer (MSP) or the process stack Pointer (PSP). Obviously, the process mode will be entered here, and using the process stack pointer (PSP), the register PC value will be restored as the task function of the task to be run, and the new task will start running! So far, the task switching is successful.

4. Find the next task to run vTaskSwitchContext()

In the PendSV interrupt service routine, the function vTaskSwitchContext() is called to obtain the next task to be run, that is, to find the task with the highest priority that is already ready. The source code of the reduced function (without conditional compilation) is as follows:

void vTaskSwitchContext( void )
{
    
    
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE ) (1)
	{
    
    
		xYieldPending = pdTRUE;
	}
	else
	{
    
    
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();
		taskCHECK_FOR_STACK_OVERFLOW();
		taskSELECT_HIGHEST_PRIORITY_TASK(); (2)
		traceTASK_SWITCHED_IN();
	}
}

(1) If the scheduler hangs, task switching cannot be performed.

(2) Call the function taskSELECT_HIGHEST_PRIORITY_TASK() to get the next task to run.
taskSELECT_HIGHEST_PRIORITY_TASK() is essentially a macro, defined in tasks.c.

There are two ways to find the next task to run in FreeRTOS: one is the general method, and the other is the method of using hardware. As for which method to choose, it is determined by the macro configUSE_PORT_OPTIMISED_TASK_SELECTION. When this macro is 1, the hardware method is used, otherwise, the general method is used. Let's take a look at the difference between these two methods.

①General method
As the name suggests, it is a method that can be used by all processors, as follows:

#define taskSELECT_HIGHEST_PRIORITY_TASK() 						 \
{
      
       																 \
	UBaseType_t uxTopPriority = uxTopReadyPriority;				 \
	while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \ (1)
	{
    
     														 	 \
		configASSERT( uxTopPriority );							 \
		--uxTopPriority; 										 \
	} 															 \
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, 					 \ (2)
	&( pxReadyTasksLists[ uxTopPriority ] ) );					 \
	uxTopReadyPriority = uxTopPriority; 						 \
} 

(1), pxReadyTasksLists[] is an array of ready task lists, one list for each priority, and ready tasks with the same priority are hung in the corresponding list. uxTopReadyPriority represents the highest priority value in the ready state. Every time a task is created, it will judge whether the priority of the new task is greater than uxTopReadyPriority. If it is greater, assign the priority of the new task to the variable uxTopReadyPriority. The function prvAddTaskToReadyList() will also modify this value, that is to say, when a task is added to the ready list, uxTopReadyPriority will be used to record the highest priority in the ready list. Here we start to judge from the highest priority, and see which list is not empty to indicate which priority has a ready task. The function listLIST_IS_EMPTY() is used to judge whether a list is empty, and uxTopPriority is used to record the priority of the ready task.

(2) Having found the priority of the ready task, the next step is to find out the next task to run from the corresponding list. The search method is to use the function listGET_OWNER_OF_NEXT_ENTRY() to get the next list item in the list. Then assign the task control block corresponding to the obtained list item to pxCurrentTCB, so that we can determine the next task to run.

It can be seen that the general method is completely implemented by C language, which is definitely applicable to different chips and platforms, and there is no limit to the number of tasks, but the efficiency is definitely much lower than that of using hardware methods.

2. Hardware method
The hardware method is implemented by using the hardware instructions that come with the processor. For example, the Cortex-M processor has the command to calculate leading zeros: CLZ, and the function is as follows:

#define taskSELECT_HIGHEST_PRIORITY_TASK()  \
{
      
       											\
	UBaseType_t uxTopPriority; 				\
	portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \ (1)
	configASSERT( listCURRENT_LIST_LENGTH( & 			\
	( pxReadyTasksLists[ uxTopPriority ] ) )> 0 ); 		\
	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB,		    \ (2)
	&( pxReadyTasksLists[ uxTopPriority ] ) ); 			\
} 

(1) Obtain the highest priority in the ready state through the function portGET_HIGHEST_PRIORITY(), portGET_HIGHEST_PRIORITY is essentially a macro, defined as follows:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL\
							  - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

When using the hardware method, uxTopReadyPriority does not represent the highest priority in the ready state, but uses each bit to represent a priority, bit0 represents priority 0, bit31 represents priority 31, when a certain priority has a ready task If so, set the corresponding bit to 1. It can be seen from here that if the hardware method is used, there can only be a maximum of 32 priorities. __clz(uxReadyPriorities) is to calculate the number of leading zeros of uxReadyPriorities. The number of leading zeros refers to the number of 0s from the highest bit (bit31) to the first bit that is 1. The following example: binary number 1000 0000 0000
0000 The number of leading zeros is 0.
The number of leading zeros in the binary number 0000 1001 1111 0001 is 4.

After getting the number of leading zeros of uxTopReadyPriority, subtract the number of leading zeros from 31 to get the highest priority in the ready state. For example, priority 30 is the highest priority in the ready state at this time, and the leading zero of 30 The number is 1, then 31-1=30, and the highest priority in the ready state is 30.

(2) The highest priority in the ready state has been found, and the next step is to find the next task to run from the corresponding list. The search method is to use the function listGET_OWNER_OF_NEXT_ENTRY() to get the next list item in the list , and then assign the task control block corresponding to the obtained list item to pxCurrentTCB, so that we can determine the next task to run.

It can be seen that the hardware method can quickly obtain the highest priority in the ready state with one instruction, but it will limit the number of priorities of the task. For example, STM32 can only have 32 priorities, but 32 priorities are enough. .

FreeRTOS supports time slices, and each priority can support an unlimited number of tasks.

Five, FreeRTOS time slice scheduling

It has been mentioned many times before that FreeRTOS supports multiple tasks with a priority at the same time. The scheduling of these tasks is a problem worth considering, but this is not what we want to consider. In FreeRTOS, a task is allowed to run for a time slice (the length of a clock tick) and then give up the right to use the CPU to let the next task with the same priority run. As for which task to run next? This scheduling method in FreeRTOS is time slice scheduling. The following figure shows the execution time graph of running at the same priority level, and there are 3 ready tasks at priority level N.

insert image description here
1. Task 3 is running.
2. At this time, a clock tick interrupt (tick timer interrupt) occurs, and the time slice of task 3 is used up, but task 3 has not been
executed yet.
3. FreeRTOS switches the task to task 1, which is the next ready task at priority N.
4. Task 1 runs continuously until the time slice runs out.
5. Task 3 obtains the CPU usage right again, and then runs.
6. When task 3 is finished, call the task switching function portYIELD() to forcibly switch the task and give up the remaining time slice, so that the next ready task under priority N can run.
7. FreeRTOS switches to task 1.
8. Task 1 finishes its time slice.

To use time slice scheduling, macro configUSE_PREEMPTION and macro configUSE_TIME_SLICING must be 1. The length of the time slice is determined by the macro configTICK_RATE_HZ. The length of a time slice is the interrupt cycle of the tick timer. For example, if configTICK_RATE_HZ is 1000 in this tutorial, the length of a time slice is 1ms. Time slice scheduling occurs in the interrupt service function of the tick timer. When explaining the interrupt service function of the tick timer, it is said that the interrupt service function SysTick_Handler() will call the API function xPortSysTickHandler() of FreeRTOS, and the function xPortSysTickHandler() will Trigger task scheduling, but this task scheduling is conditional, the function xPortSysTickHandler() is as follows:

void xPortSysTickHandler( void )
{
    
    
	vPortRaiseBASEPRI();
	{
    
    
		if( xTaskIncrementTick() != pdFALSE )
		{
    
    
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

The above code if( xTaskIncrementTick() != pdFALSE )shows that only when the return value of the function xTaskIncrementTick() is not pdFALSE, the task scheduling will be carried out! Looking at the function xTaskIncrementTick(), you will find the following conditional compilation statement:

BaseType_t xTaskIncrementTick( void )
{
    
    
	TCB_t * pxTCB;
	TickType_t xItemValue;
	BaseType_t xSwitchRequired = pdFALSE;
	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
	{
    
    
		/***************************************************************************/
		/***************************此处省去一大堆代码******************************/
		/***************************************************************************/
		#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) (1)
		{
    
    
			if( listCURRENT_LIST_LENGTH( &( \
			pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) (2)
			{
    
    
				xSwitchRequired = pdTRUE; (3)
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
	}
	return xSwitchRequired;
}

(1) The following code will compile only when both macro configUSE_PREEMPTION and macro configUSE_PREEMPTION are 1. So if you want to use time slice scheduling, these two macros must be 1, and both are indispensable!

(2) Determine whether there are other tasks under the priority corresponding to the current task.

(3) If there are other tasks under the task priority corresponding to the current task, pdTRUE will be returned.

As can be seen from the above code, if there are other tasks under the priority corresponding to the current task, the function
xTaskIncrementTick() will return pdTURE, and since the function return value is pdTURE, the function xPortSysTickHandler() will perform a task switch .

6. Time slice scheduling experiment

1. The purpose of the experiment
To learn to use the time slice scheduling of FreeRTOS.

2. Experiment design
Three tasks are designed in this experiment: start_task, task1_task and task2_task, among which task1_task and task2_task have the same task priority, which is 2, and the task functions of these three tasks are as follows: start_task: used to create other 2 tasks
.
task1_task : Control LED0 to blink, and print the running times of task1_task through the serial port.
task2_task: Control LED1 to blink, and print the running times of task2_task through the serial port.

3. Experimental procedure and analysis

● System settings
For the convenience of observation, set the clock tick frequency of the system to 20, that is, set the macro configTICK_RATE_HZ to 20:

#define configTICK_RATE_HZ (20) 

After this setting, the interrupt period of the tick timer is 50ms, that is to say, the time slice value is 50ms. This time slice is still very large, but it is convenient for us to observe when it is bigger.

● Task settings

#define START_TASK_PRIO 1 //任务优先级
#define START_STK_SIZE 128 //任务堆栈大小
TaskHandle_t StartTask_Handler; //任务句柄
void start_task(void *pvParameters); //任务函数
#define TASK1_TASK_PRIO 2 //任务优先级 (1)
#define TASK1_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task1Task_Handler; //任务句柄
void task1_task(void *pvParameters); //任务函数
#define TASK2_TASK_PRIO 2 //任务优先级 (2)
#define TASK2_STK_SIZE 128 //任务堆栈大小
TaskHandle_t Task2Task_Handler; //任务句柄
void task2_task(void *pvParameters); //任务函数

(1) and (2), the task priority of tasks task1_task and task2_task is set to be the same, here they are both set to 2.

● main() function

int main(void)
{
    
    
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组 4
	delay_init(); //延时函数初始化
	uart_init(115200); //初始化串口
	LED_Init(); //初始化 LED
	LCD_Init(); //初始化 LCD
	POINT_COLOR = RED;
	LCD_ShowString(30,10,200,16,16,"ATK STM32F103/407");
	LCD_ShowString(30,30,200,16,16,"FreeRTOS Examp 9-1");
	LCD_ShowString(30,50,200,16,16,"FreeRTOS Round Robin");
	LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,90,200,16,16,"2016/11/25");
	 //创建开始任务
	 xTaskCreate((TaskFunction_t )start_task, //任务函数
	 (const char* )"start_task", //任务名称
	 (uint16_t )START_STK_SIZE, //任务堆栈大小
	 (void* )NULL, //传递给任务函数的参数
	 (UBaseType_t )START_TASK_0PRIO, //任务优先级
	 (TaskHandle_t* )&StartTask_Handler); //任务句柄 
	 vTaskStartScheduler(); //开启任务调度
}

In the main function, we mainly complete the hardware initialization. After the hardware initialization is completed, the task start_task is created and the task scheduling of FreeRTOS is started.

● task function

//开始任务任务函数
void start_task(void *pvParameters)
{
    
    
	taskENTER_CRITICAL(); //进入临界区
	//创建 TASK1 任务
	xTaskCreate((TaskFunction_t )task1_task, 
	(const char* )"task1_task", 
	(uint16_t )TASK1_STK_SIZE, 
	(void* )NULL, 
	(UBaseType_t )TASK1_TASK_PRIO, 
	(TaskHandle_t* )&Task1Task_Handler); 
	//创建 TASK2 任务
	xTaskCreate((TaskFunction_t )task2_task, 
	(const char* )"task2_task", 
	(uint16_t )TASK2_STK_SIZE,
	(void* )NULL,
	(UBaseType_t )TASK2_TASK_PRIO,
	(TaskHandle_t* )&Task2Task_Handler); 
	vTaskDelete(StartTask_Handler); //删除开始任务
	taskEXIT_CRITICAL(); //退出临界区
}

//task1 任务函数
void task1_task(void *pvParameters)
{
    
    
	u8 task1_num=0;
	while(1)
	{
    
    
		task1_num++; //任务 1 执行次数加 1 注意 task1_num1 加到 255 的时候会清零!!
		LED0=!LED0;
		taskENTER_CRITICAL(); //进入临界区
		printf("任务 1 已经执行:%d 次\r\n",task1_num);
		taskEXIT_CRITICAL(); //退出临界区
		//延时 10ms,模拟任务运行 10ms,此函数不会引起任务调度
		delay_xms(10); (1)
	}
}

//task2 任务函数
void task2_task(void *pvParameters)
{
    
    
	u8 task2_num=0;
	while(1)
	{
    
    
		task2_num++; //任务 2 执行次数加 1 注意 task2_num1 加到 255 的时候会清零!!
		 LED1=!LED1;
		taskENTER_CRITICAL(); //进入临界区
		printf("任务 2 已经执行:%d 次\r\n",task2_num);
		taskEXIT_CRITICAL(); //退出临界区
		//延时 10ms,模拟任务运行 10ms,此函数不会引起任务调度
		delay_xms(10); (2)
	}
}

(1) Call the function delay_xms() to delay 10ms. If the task does not voluntarily give up the right to use the CPU within a time slice, it will keep running this task until the time slice is exhausted. In the task1_task task, we print a string through the serial port to prompt that task1_task is running, but this process is fast for the CPU, which is not conducive to observation, so the default task occupies 10ms of CPU by calling the function delay_xms(). The function delay_xm() will not cause task scheduling. In this case, the execution period of task1_task is greater than 10ms, which can basically be regarded as equal to 10ms, because the execution speed of other functions is still very fast. The length of a time slice is 50ms, and the time required for task execution is calculated as 10ms. In theory, task1_task can be executed 5 times in a time slice, but in fact it is rarely executed 5 times, basically 4 times.

(2), the same reason (1)

4. The experimental phenomenon
insert image description here
whether it is task1_task or task2_task is executed continuously for 4 or 5 times, which is the same as the previous program design, indicating that a task is always running in a time slice, and it will switch to the next task when the time slice is used up. .

Guess you like

Origin blog.csdn.net/Dustinthewine/article/details/130098201